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

github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKirill Osenkov <github@osenkov.com>2018-12-06 02:09:58 +0300
committerKirill Osenkov <github@osenkov.com>2018-12-06 02:09:58 +0300
commit30e88b1f2ce3ee9897e1f3b87c80c64292c27236 (patch)
tree45ec1d0b3e72b459533fdff426f7cfc958ea87f7 /src
parent501ae272174261c9d8a60159e0a162a0af4f8922 (diff)
Update to VS-Platform 16.0.142-g25b7188c54.
25b7188c54f0cdf6a5be87eeb38e3c6046d5788e
Diffstat (limited to 'src')
-rw-r--r--src/Core/Def/BaseUtility/IGuardedOperations.cs9
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionDataSnapshot.cs20
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionInitialDataSnapshot.cs12
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/CommitBehavior.cs2
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterWithState.cs14
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs1
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/CompletionParticipation.cs34
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/CompletionStartData.cs50
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/CompletionTrigger.cs68
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/CompletionTriggerReason.cs50
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/InitialTrigger.cs52
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/InitialTriggerReason.cs35
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/SuggestionItemOptions.cs5
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/UpdateTrigger.cs50
-rw-r--r--src/Language/Def/Language/AsyncCompletion/Data/UpdateTriggerReason.cs31
-rw-r--r--src/Language/Def/Language/AsyncCompletion/IAsyncCompletionBroker.cs10
-rw-r--r--src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManager.cs56
-rw-r--r--src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManager.cs11
-rw-r--r--src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs6
-rw-r--r--src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs (renamed from src/Language/Impl/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs)13
-rw-r--r--src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs32
-rw-r--r--src/Language/Def/Language/AsyncCompletion/ICompletionPresenter.cs7
-rw-r--r--src/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs7
-rw-r--r--src/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs63
-rw-r--r--src/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs352
-rw-r--r--src/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs151
-rw-r--r--src/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs40
-rw-r--r--src/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs30
-rw-r--r--src/Language/Impl/Language/AsyncCompletion/ModelComputation.cs15
-rw-r--r--src/Language/Impl/Language/AsyncCompletion/PrioritizedTaskScheduler.cs13
-rw-r--r--src/Language/Impl/Language/AsyncCompletion/SuggestionModeCompletionItemSource.cs9
-rw-r--r--src/Language/Impl/Language/Strings.Designer.cs13
-rw-r--r--src/Language/Impl/Language/Strings.resx4
-rw-r--r--src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs40
-rw-r--r--src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs11
-rw-r--r--src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs20
-rw-r--r--src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs20
-rw-r--r--src/Text/Def/Internal/TextLogic/ParentAttribute.cs20
-rw-r--r--src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs22
-rw-r--r--src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs142
-rw-r--r--src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs7
-rw-r--r--src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs16
-rw-r--r--src/Text/Def/TextUI/Editor/ITextView2.cs51
-rw-r--r--src/Text/Def/TextUI/Editor/TextViewExtensions.cs71
-rw-r--r--src/Text/Def/TextUI/EditorOptions/ViewOptions.cs28
-rw-r--r--src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs8
-rw-r--r--src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs100
-rw-r--r--src/Text/Def/TextUI/Utilities/IStatusBarService.cs22
-rw-r--r--src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs22
-rw-r--r--src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs44
-rw-r--r--src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs64
-rw-r--r--src/Text/Impl/Commanding/CommandingStrings.Designer.cs11
-rw-r--r--src/Text/Impl/Commanding/CommandingStrings.resx3
-rw-r--r--src/Text/Impl/Commanding/EditorCommandHandlerService.cs211
-rw-r--r--src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs45
-rw-r--r--src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs55
-rw-r--r--src/Text/Impl/EditorOperations/EditorOperations.cs653
-rw-r--r--src/Text/Impl/EditorOptions/EditorOptions.cs13
-rw-r--r--src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs34
-rw-r--r--src/Text/Impl/Outlining/OutliningManager.cs45
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs78
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs71
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs256
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs69
-rw-r--r--src/Text/Util/TextDataUtil/GuardedOperations.cs24
-rw-r--r--src/Text/Util/TextLogicUtil/SideBySideMap.cs136
-rw-r--r--src/Text/Util/TextUIUtil/ExtensionMethods.cs16
67 files changed, 2885 insertions, 808 deletions
diff --git a/src/Core/Def/BaseUtility/IGuardedOperations.cs b/src/Core/Def/BaseUtility/IGuardedOperations.cs
index 1a94a38..6180a0e 100644
--- a/src/Core/Def/BaseUtility/IGuardedOperations.cs
+++ b/src/Core/Def/BaseUtility/IGuardedOperations.cs
@@ -262,6 +262,15 @@ namespace Microsoft.VisualStudio.Utilities
/// <remarks>This class supports the Visual Studio
/// infrastructure and in general is not intended to be used directly from your code.</remarks>
Task RaiseEventOnBackgroundAsync<TArgs>(object sender, AsyncEventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs;
+
+ /// <summary>
+ /// Safely attempts to cast the given object to the given type.
+ /// </summary>
+ /// <typeparam name="TArgs">The type that should be casted to.</typeparam>
+ /// <param name="toCast">The object that should be casted.</param>
+ /// <param name="casted">Returns out the casted object or default(TArgs) if the cast failed.</param>
+ /// <returns>True if successful in casting, false otherwise.</returns>
+ bool TryCastToType<TArgs>(object toCast, out TArgs casted);
#pragma warning restore CA1030 // Use events where appropriate
}
}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionDataSnapshot.cs b/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionDataSnapshot.cs
index d947d61..eeaeded 100644
--- a/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionDataSnapshot.cs
+++ b/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionDataSnapshot.cs
@@ -4,7 +4,7 @@ using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
{
/// <summary>
- /// Contains data of <see cref="IAsyncCompletionSession"/> valid at a specific, instantenous moment pertinent to current computation.
+ /// Contains data of <see cref="IAsyncCompletionSession"/> valid at a specific, instantaneous moment pertinent to current computation.
/// This data is passed to <see cref="IAsyncCompletionItemManager"/> to filter the list and select appropriate item.
/// </summary>
public class AsyncCompletionSessionDataSnapshot
@@ -20,14 +20,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
public ITextSnapshot Snapshot { get; }
/// <summary>
- /// The <see cref="InitialTrigger"/> that started this completion session.
+ /// The <see cref="CompletionTrigger"/> that caused this update.
/// </summary>
- public InitialTrigger InitialTrigger { get; }
-
- /// <summary>
- /// The <see cref="UpdateTrigger"/> for this update.
- /// </summary>
- public UpdateTrigger UpdateTrigger { get; }
+ public CompletionTrigger Trigger { get; }
/// <summary>
/// Filters, their availability and selection state.
@@ -49,16 +44,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
/// </summary>
/// <param name="initialSortedList">Set of <see cref="CompletionItem"/>s to filter and sort, originally returned from <see cref="IAsyncCompletionItemManager.SortCompletionListAsync"/></param>
/// <param name="snapshot">The <see cref="ITextSnapshot"/> applicable for this computation. The snapshot comes from the view's data buffer</param>
- /// <param name="initialTrigger">The <see cref="InitialTrigger"/> that started this completion session</param>
- /// <param name="updateTrigger">The <see cref="UpdateTrigger"/> for this update</param>
+ /// <param name="trigger">The <see cref="CompletionTrigger"/> that caused this update</param>
/// <param name="selectedFilters">Filters, their availability and selection state</param>
/// <param name="isSoftSelected">Inidicates whether the session is using soft selection</param>
/// <param name="displaySuggestionItem">Inidicates whether the session has a suggestion item</param>
public AsyncCompletionSessionDataSnapshot(
ImmutableArray<CompletionItem> initialSortedList,
ITextSnapshot snapshot,
- InitialTrigger initialTrigger,
- UpdateTrigger updateTrigger,
+ CompletionTrigger trigger,
ImmutableArray<CompletionFilterWithState> selectedFilters,
bool isSoftSelected,
bool displaySuggestionItem
@@ -66,8 +59,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
{
InitialSortedList = initialSortedList;
Snapshot = snapshot;
- InitialTrigger = initialTrigger;
- UpdateTrigger = updateTrigger;
+ Trigger = trigger;
SelectedFilters = selectedFilters;
IsSoftSelected = isSoftSelected;
DisplaySuggestionItem = displaySuggestionItem;
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionInitialDataSnapshot.cs b/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionInitialDataSnapshot.cs
index 8b3f3dd..40d26a3 100644
--- a/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionInitialDataSnapshot.cs
+++ b/src/Language/Def/Language/AsyncCompletion/Data/AsyncCompletionSessionInitialDataSnapshot.cs
@@ -4,7 +4,7 @@ using Microsoft.VisualStudio.Text;
namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
{
/// <summary>
- /// Contains data of <see cref="IAsyncCompletionSession"/> valid at a specific, instantenous moment pertinent to current computation.
+ /// Contains data of <see cref="IAsyncCompletionSession"/> valid at a specific, instantaneous moment pertinent to current computation.
/// This data is passed to <see cref="IAsyncCompletionItemManager"/> to initially sort the list prior to filtering and selecting.
/// </summary>
public class AsyncCompletionSessionInitialDataSnapshot
@@ -20,25 +20,25 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
public ITextSnapshot Snapshot { get; }
/// <summary>
- /// The <see cref="InitialTrigger"/> that started this completion session.
+ /// The <see cref="CompletionTrigger"/> that started this completion session.
/// </summary>
- public InitialTrigger InitialTrigger { get; }
+ public CompletionTrigger Trigger { get; }
/// <summary>
/// Constructs <see cref="AsyncCompletionSessionInitialDataSnapshot"/>
/// </summary>
/// <param name="initialList">Set of <see cref="CompletionItem"/>s to sort</param>
/// <param name="snapshot">The <see cref="ITextSnapshot"/> applicable for this computation. The snapshot comes from the view's data buffer</param>
- /// <param name="initialTrigger">The <see cref="InitialTrigger"/> that started this completion session</param>
+ /// <param name="trigger">The <see cref="CompletionTrigger"/> that started this completion session</param>
public AsyncCompletionSessionInitialDataSnapshot(
ImmutableArray<CompletionItem> initialList,
ITextSnapshot snapshot,
- InitialTrigger initialTrigger
+ CompletionTrigger trigger
)
{
InitialList = initialList;
Snapshot = snapshot;
- InitialTrigger = initialTrigger;
+ Trigger = trigger;
}
}
}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CommitBehavior.cs b/src/Language/Def/Language/AsyncCompletion/Data/CommitBehavior.cs
index 659fa2a..b2e1503 100644
--- a/src/Language/Def/Language/AsyncCompletion/Data/CommitBehavior.cs
+++ b/src/Language/Def/Language/AsyncCompletion/Data/CommitBehavior.cs
@@ -29,7 +29,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
RaiseFurtherReturnKeyAndTabKeyCommandHandlers = 0b0010,
/// <summary>
- /// Cancels the commit operation, does not call any other <see cref="IAsyncCompletionCommitManager.TryCommit(Text.Editor.ITextView, Text.ITextBuffer, CompletionItem, Text.ITrackingSpan, char, System.Threading.CancellationToken)"/>.
+ /// Cancels the commit operation, does not call any other <see cref="IAsyncCompletionCommitManager.TryCommit(IAsyncCompletionSession, Text.ITextBuffer, CompletionItem, char, System.Threading.CancellationToken)"/>.
/// Functionally, acts as if the typed character was not a commit character,
/// allowing the user to continue working with the <see cref="IAsyncCompletionSession"/>
/// </summary>
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterWithState.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterWithState.cs
index 7712e93..66ea895 100644
--- a/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterWithState.cs
+++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionFilterWithState.cs
@@ -3,7 +3,8 @@
namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
{
/// <summary>
- /// Immutable data transfer object used to communicate between the completion session and completion UI
+ /// Immutable data transfer object that describes state of a <see cref="CompletionFilter"/>:
+ /// whether it <see cref="IsAvailable"/> based on typed text and whether it <see cref="IsSelected"/> by the user.
/// </summary>
public sealed class CompletionFilterWithState
{
@@ -14,17 +15,19 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
/// <summary>
/// Whether the filter is available.
- /// Filter should be available when there are visible <see cref="CompletionItem"/>s that define this <see cref="Filter"/> in their <see cref="CompletionItem.Filters"/>
+ /// A filter is available if after filtering by entered text, there are any <see cref="CompletionItem"/>s that reference this <see cref="Filter"/> in their <see cref="CompletionItem.Filters"/>
+ /// Filtering <see cref="CompletionItem"/>s by toggling <see cref="IsSelected"/> property of the <see cref="CompletionFilter"/>s has no impact on this availability.
/// </summary>
public bool IsAvailable { get; }
/// <summary>
/// Whether the filter is selected by the user.
+ /// User may select a filter using mouse or a keyboard shortcut.
/// </summary>
public bool IsSelected { get; }
/// <summary>
- /// Constructs a new instance of <see cref="CompletionFilterWithState"/>.
+ /// Constructs a new instance of <see cref="CompletionFilterWithState"/> which is not selected.
/// </summary>
/// <param name="filter">Reference to <see cref="CompletionFilter"/></param>
/// <param name="isAvailable">Whether this <see cref="CompletionFilter"/> is available</param>
@@ -46,7 +49,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
}
/// <summary>
- /// Returns instance of <see cref="CompletionFilterWithState"/> with specified <see cref="IsAvailable"/>
+ /// Returns instance of <see cref="CompletionFilterWithState"/> with specified <see cref="IsAvailable"/>.
+ /// Use this method when entered text changes availability of relevant <see cref="CompletionItem"/>s.
/// </summary>
/// <param name="isAvailable">Value to use for <see cref="IsAvailable"/></param>
/// <returns>Updated instance of <see cref="CompletionFilterWithState"/></returns>
@@ -70,7 +74,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
}
/// <summary>
- /// Override for nice debugger display
+ /// Override for debugger display
/// </summary>
public override string ToString()
{
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs
index 0591dd3..2539c82 100644
--- a/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs
+++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionItemWithHighlight.cs
@@ -42,6 +42,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
CompletionItem = completionItem ?? throw new ArgumentNullException(nameof(completionItem));
if (highlightedSpans.IsDefault)
throw new ArgumentException("Array must be initialized", nameof(highlightedSpans));
+
HighlightedSpans = highlightedSpans;
}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionParticipation.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionParticipation.cs
new file mode 100644
index 0000000..0e6d142
--- /dev/null
+++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionParticipation.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
+{
+ /// <summary>
+ /// Describes the level of <see cref="IAsyncCompletionSource"/>'s participation in the <see cref="IAsyncCompletionSession"/>.
+ /// </summary>
+ public enum CompletionParticipation
+ {
+ /// <summary>
+ /// This <see cref="IAsyncCompletionSource"/> will not provide completion items.
+ /// <see cref="CompletionStartData.ApplicableToSpan"/> returned by this <see cref="IAsyncCompletionSource"/> may be used
+ /// in the prospective <see cref="IAsyncCompletionSession"/> if another <see cref="IAsyncCompletionSource"/> announced
+ /// participation in completion.
+ /// </summary>
+ DoesNotProvideItems = 0,
+
+ /// <summary>
+ /// <see cref="IAsyncCompletionSource.GetCompletionContextAsync(IAsyncCompletionSession, CompletionTrigger, Text.SnapshotPoint, Text.SnapshotSpan, System.Threading.CancellationToken)"/>
+ /// will be invoked, unless another <see cref="IAsyncCompletionSource"/>s returned <see cref="CompletionParticipation.ExclusivelyProvidesItems"/>.
+ /// </summary>
+ ProvidesItems = 1,
+
+ /// <summary>
+ /// <see cref="IAsyncCompletionSource.GetCompletionContextAsync(IAsyncCompletionSession, CompletionTrigger, Text.SnapshotPoint, Text.SnapshotSpan, System.Threading.CancellationToken)"/>
+ /// will be invoked only on this <see cref="IAsyncCompletionSource"/> and other <see cref="IAsyncCompletionSource"/>s which returned <see cref="CompletionParticipation.ExclusivelyProvidesItems"/>.
+ /// </summary>
+ ExclusivelyProvidesItems = 2,
+ }
+}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionStartData.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionStartData.cs
new file mode 100644
index 0000000..b9c6766
--- /dev/null
+++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionStartData.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Diagnostics;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
+{
+ [DebuggerDisplay("{Participation}")]
+ public struct CompletionStartData : IEquatable<CompletionStartData>
+ {
+ /// <summary>
+ /// Value to use when <see cref="IAsyncCompletionSource"/> does not know the precise <see cref="SnapshotSpan"/> for completion,
+ /// and does not want to participate in completion.
+ /// </summary>
+ public static CompletionStartData DoesNotParticipateInCompletion { get; } = new CompletionStartData(CompletionParticipation.DoesNotProvideItems, default);
+
+ /// <summary>
+ /// Value to use when <see cref="IAsyncCompletionSource"/> does not know the precise <see cref="SnapshotSpan"/> for completion,
+ /// but wishes to participate in completion if language service can provide a valid <see cref="SnapshotSpan"/>.
+ /// </summary>
+ public static CompletionStartData ParticipatesInCompletionIfAny { get; } = new CompletionStartData(CompletionParticipation.ProvidesItems, default);
+
+ /// <summary>
+ /// Describes the level of <see cref="IAsyncCompletionSource"/>'s participation in the <see cref="IAsyncCompletionSession"/>.
+ /// </summary>
+ public CompletionParticipation Participation { get; }
+
+ /// <summary>
+ /// <param name="applicableToSpan"> Proposed location where completion will take place.
+ /// Return <code>default</code> if this <see cref="IAsyncCompletionSource"/> is not capable of providing location,
+ /// or completion is invalid for location in question.</param>
+ /// </summary>
+ public SnapshotSpan ApplicableToSpan { get; }
+
+ public CompletionStartData(CompletionParticipation participation, SnapshotSpan applicableToSpan = default) : this()
+ {
+ Participation = participation;
+ ApplicableToSpan = applicableToSpan;
+ }
+
+ bool IEquatable<CompletionStartData>.Equals(CompletionStartData other) => Participation.Equals(other.Participation) && ApplicableToSpan.Equals(other.ApplicableToSpan);
+
+ public override bool Equals(object other) => (other is CompletionStartData otherCR) ? ((IEquatable<CompletionStartData>)this).Equals(otherCR) : false;
+
+ public static bool operator ==(CompletionStartData left, CompletionStartData right) => left.Equals(right);
+
+ public static bool operator !=(CompletionStartData left, CompletionStartData right) => !(left == right);
+
+ public override int GetHashCode() => (ApplicableToSpan.GetHashCode() << 2) | ((int)Participation);
+ }
+}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionTrigger.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionTrigger.cs
new file mode 100644
index 0000000..0986489
--- /dev/null
+++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionTrigger.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Diagnostics;
+using Microsoft.VisualStudio.Text;
+
+namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
+{
+ /// <summary>
+ /// What caused the completion to trigger or update.
+ /// Location is not provided in this struct because Editor maps the location
+ /// to an appropriate buffer for each <see cref="IAsyncCompletionSource"/>.
+ /// </summary>
+ [DebuggerDisplay("{Reason} {Character}")]
+ public struct CompletionTrigger : IEquatable<CompletionTrigger>
+ {
+ /// <summary>
+ /// The reason that completion action was taken.
+ /// </summary>
+ public CompletionTriggerReason Reason { get; }
+
+ /// <summary>
+ /// The text edit associated with the action.
+ /// </summary>
+ public char Character { get; }
+
+ /// <summary>
+ /// <see cref="ITextSnapshot"/> on the view's text buffer before the completion action was taken.
+ /// For <see cref="CompletionTriggerReason.Backspace"/>, <see cref="CompletionTriggerReason.Deletion"/> and <see cref="CompletionTriggerReason.Insertion"/>,
+ /// this is text snapshot before the edit has been made. You may use it to get higher fidelity data on text edits that led to this action.
+ /// If there was no edit, or edit is unavailable, this is the <see cref="ITextSnapshot"/> at the time action happened.
+ /// Take precaution when accessing this property: since this is a struct, it may be left uninitialized.
+ /// </summary>
+ public ITextSnapshot ViewSnapshotBeforeTrigger { get; }
+
+ /// <summary>
+ /// Creates a <see cref="CompletionTrigger"/> not associated with a text edit
+ /// </summary>
+ /// <param name="reason">The kind of action that triggered completion action</param>
+ /// <param name="snapshotBeforeTrigger">Snapshot on the view's text buffer when action was taken</param>
+ public CompletionTrigger(CompletionTriggerReason reason, ITextSnapshot snapshotBeforeTrigger) : this(reason, snapshotBeforeTrigger, default)
+ { }
+
+ /// <summary>
+ /// Creates a <see cref="CompletionTrigger"/> associated with a text edit
+ /// </summary>
+ /// <param name="reason">The kind of action that caused completion to trigger or update</param>
+ /// <param name="character">Character associated with the action</param>
+ /// <param name="snapshotBeforeTrigger">Snapshot on the view's text buffer before or when action was taken</param>
+ public CompletionTrigger(CompletionTriggerReason reason, ITextSnapshot snapshotBeforeTrigger, char character)
+ {
+ this.Reason = reason;
+ this.Character = character;
+ this.ViewSnapshotBeforeTrigger = snapshotBeforeTrigger ?? throw new ArgumentNullException(nameof(snapshotBeforeTrigger));
+ }
+
+ bool IEquatable<CompletionTrigger>.Equals(CompletionTrigger other) =>
+ Reason.Equals(other.Reason)
+ && Character.Equals(other.Character)
+ && ViewSnapshotBeforeTrigger.Equals(other.ViewSnapshotBeforeTrigger);
+
+ public override bool Equals(object other) => (other is CompletionTrigger otherTrigger) ? ((IEquatable<CompletionTrigger>)this).Equals(otherTrigger) : false;
+
+ public static bool operator ==(CompletionTrigger left, CompletionTrigger right) => left.Equals(right);
+
+ public static bool operator !=(CompletionTrigger left, CompletionTrigger right) => !(left == right);
+
+ public override int GetHashCode() => Reason.GetHashCode() ^ Character.GetHashCode();
+ }
+}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/CompletionTriggerReason.cs b/src/Language/Def/Language/AsyncCompletion/Data/CompletionTriggerReason.cs
new file mode 100644
index 0000000..fd645e6
--- /dev/null
+++ b/src/Language/Def/Language/AsyncCompletion/Data/CompletionTriggerReason.cs
@@ -0,0 +1,50 @@
+namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
+{
+ /// <summary>
+ /// Describes the kind of action that initially triggered completion to open.
+ /// </summary>
+ public enum CompletionTriggerReason
+ {
+ /// <summary>
+ /// Completion was triggered by a direct invocation of the completion feature
+ /// using the Edit.ListMember command.
+ /// </summary>
+ Invoke = 0,
+
+ /// <summary>
+ /// Completion was triggered with a request to commit if a single item would be selected
+ /// using the Edit.CompleteWord command.
+ /// </summary>
+ InvokeAndCommitIfUnique = 1,
+
+ /// <summary>
+ /// Completion was triggered with a request to display items of matching type
+ /// </summary>
+ InvokeMatchingType = 2,
+
+ /// <summary>
+ /// Completion was triggered or updated via an action inserting a character into the buffer.
+ /// </summary>
+ Insertion = 3,
+
+ /// <summary>
+ /// Completion was triggered or updated by removing a character from the buffer using Delete.
+ /// </summary>
+ Deletion = 4,
+
+ /// <summary>
+ /// Completion was triggered or updated by removing a character from the buffer using Backspace.
+ /// </summary>
+ Backspace = 5,
+
+ /// <summary>
+ /// Completion was updated by changing filters
+ /// </summary>
+ FilterChange = 6,
+
+ /// <summary>
+ /// Completion was triggered by Roslyn's Snippets mode
+ /// </summary>
+ SnippetsMode = 7
+ }
+}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/InitialTrigger.cs b/src/Language/Def/Language/AsyncCompletion/Data/InitialTrigger.cs
deleted file mode 100644
index 9319b3e..0000000
--- a/src/Language/Def/Language/AsyncCompletion/Data/InitialTrigger.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using System;
-using System.Diagnostics;
-
-namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
-{
- /// <summary>
- /// What triggered the completion, but not where it happened.
- /// The reason we don't expose location is that for each extension,
- /// we map the point to a buffer with matching content type.
- /// </summary>
- [DebuggerDisplay("{Reason} {Character}")]
- public struct InitialTrigger : IEquatable<InitialTrigger>
- {
- /// <summary>
- /// The reason that completion was started.
- /// </summary>
- public InitialTriggerReason Reason { get; }
-
- /// <summary>
- /// The text edit associated with the triggering action.
- /// </summary>
- public char Character { get; }
-
- /// <summary>
- /// Creates a <see cref="InitialTrigger"/> associated with a text edit
- /// </summary>
- /// <param name="reason">The kind of action that triggered completion to start</param>
- /// <param name="character">Character that triggered completion</param>
- public InitialTrigger(InitialTriggerReason reason, char character)
- {
- this.Reason = reason;
- this.Character = character;
- }
-
- /// <summary>
- /// Creates a <see cref="InitialTrigger"/> not associated with a text edit
- /// </summary>
- /// <param name="reason">The kind of action that triggered completion to start</param>
- public InitialTrigger(InitialTriggerReason reason) : this(reason, default)
- { }
-
- bool IEquatable<InitialTrigger>.Equals(InitialTrigger other) => Reason.Equals(other.Reason) && Character.Equals(other.Character);
-
- public override bool Equals(object other) => (other is InitialTrigger otherImage) ? ((IEquatable<InitialTrigger>)this).Equals(otherImage) : false;
-
- public static bool operator ==(InitialTrigger left, InitialTrigger right) => left.Equals(right);
-
- public static bool operator !=(InitialTrigger left, InitialTrigger right) => !(left == right);
-
- public override int GetHashCode() => Reason.GetHashCode() ^ Character.GetHashCode();
- }
-}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/InitialTriggerReason.cs b/src/Language/Def/Language/AsyncCompletion/Data/InitialTriggerReason.cs
deleted file mode 100644
index ec1942e..0000000
--- a/src/Language/Def/Language/AsyncCompletion/Data/InitialTriggerReason.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
-{
- /// <summary>
- /// Describes the kind of action that initially triggered completion to open.
- /// </summary>
- public enum InitialTriggerReason
- {
- /// <summary>
- /// Completion was triggered by a direct invocation of the completion feature
- /// using the Edit.ListMember command.
- /// </summary>
- Invoke,
-
- /// <summary>
- /// Completion was triggered with a request to commit if a single item would be selected
- /// using the Edit.CompleteWord command.
- /// </summary>
- InvokeAndCommitIfUnique,
-
- /// <summary>
- /// Completion was triggered via an action inserting a character into the document.
- /// </summary>
- Insertion,
-
- /// <summary>
- /// Completion was triggered via an action deleting a character from the document.
- /// </summary>
- Deletion,
-
- /// <summary>
- /// Completion was triggered for snippets only.
- /// </summary>
- Snippets,
- }
-}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/SuggestionItemOptions.cs b/src/Language/Def/Language/AsyncCompletion/Data/SuggestionItemOptions.cs
index a9550e5..90e6b0f 100644
--- a/src/Language/Def/Language/AsyncCompletion/Data/SuggestionItemOptions.cs
+++ b/src/Language/Def/Language/AsyncCompletion/Data/SuggestionItemOptions.cs
@@ -32,11 +32,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
/// <param name="toolTipText">Localized tooltip text for the suggestion item</param>
public SuggestionItemOptions(string displayTextWhenEmpty, string toolTipText)
{
- if (string.IsNullOrWhiteSpace(toolTipText))
- throw new ArgumentNullException(nameof(toolTipText));
-
DisplayTextWhenEmpty = displayTextWhenEmpty ?? throw new ArgumentNullException(nameof(displayTextWhenEmpty));
- ToolTipText = toolTipText;
+ ToolTipText = toolTipText ?? throw new ArgumentNullException(nameof(toolTipText));
}
}
}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/UpdateTrigger.cs b/src/Language/Def/Language/AsyncCompletion/Data/UpdateTrigger.cs
deleted file mode 100644
index 6151d88..0000000
--- a/src/Language/Def/Language/AsyncCompletion/Data/UpdateTrigger.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using System;
-using System.Diagnostics;
-
-namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
-{
- /// <summary>
- /// What triggered updating of completion.
- /// </summary>
- [DebuggerDisplay("{Reason} {Character}")]
- public struct UpdateTrigger : IEquatable<UpdateTrigger>
- {
- /// <summary>
- /// The reason that completion was updated.
- /// </summary>
- public UpdateTriggerReason Reason { get; }
-
- /// <summary>
- /// The text edit associated with the triggering action.
- /// </summary>
- public char Character { get; }
-
- /// <summary>
- /// Creates a <see cref="UpdateTrigger"/> associated with a text edit
- /// </summary>
- /// <param name="reason">The kind of action that triggered completion to update</param>
- /// <param name="character">Character that triggered the update</param>
- public UpdateTrigger(UpdateTriggerReason reason, char character)
- {
- this.Reason = reason;
- this.Character = character;
- }
-
- /// <summary>
- /// Creates a <see cref="InitialTrigger"/> not associated with a text edit
- /// </summary>
- /// <param name="reason">The kind of action that triggered completion to update</param>
- public UpdateTrigger(UpdateTriggerReason reason) : this(reason, default(char))
- { }
-
- bool IEquatable<UpdateTrigger>.Equals(UpdateTrigger other) => Reason.Equals(other.Reason) && Character.Equals(other.Character);
-
- public override bool Equals(object other) => (other is InitialTrigger otherImage) ? ((IEquatable<UpdateTrigger>)this).Equals(otherImage) : false;
-
- public static bool operator ==(UpdateTrigger left, UpdateTrigger right) => left.Equals(right);
-
- public static bool operator !=(UpdateTrigger left, UpdateTrigger right) => !(left == right);
-
- public override int GetHashCode() => Reason.GetHashCode() ^ Character.GetHashCode();
- }
-}
diff --git a/src/Language/Def/Language/AsyncCompletion/Data/UpdateTriggerReason.cs b/src/Language/Def/Language/AsyncCompletion/Data/UpdateTriggerReason.cs
deleted file mode 100644
index 83ce4d1..0000000
--- a/src/Language/Def/Language/AsyncCompletion/Data/UpdateTriggerReason.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-
-namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data
-{
- /// <summary>
- /// Describes the kind of action that triggered completion to filter.
- /// </summary>
- public enum UpdateTriggerReason
- {
- /// <summary>
- /// Completion was triggered by a direct invocation of the completion feature
- /// using the Edit.ListMember command.
- /// </summary>
- Initial,
-
- /// <summary>
- /// Completion was triggered via an action inserting a character into the document.
- /// </summary>
- Insertion,
-
- /// <summary>
- /// Completion was triggered via an action deleting a character from the document.
- /// </summary>
- Deletion,
-
- /// <summary>
- /// Update was triggered by changing filters
- /// </summary>
- FilterChange,
- }
-}
diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionBroker.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionBroker.cs
index f882166..8970a3d 100644
--- a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionBroker.cs
+++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionBroker.cs
@@ -32,7 +32,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// <summary>
/// Returns whether there are any completion item sources available for given <see cref="IContentType"/>.
- /// This method should be called prior to calling <see cref="TriggerCompletion(ITextView, SnapshotPoint, char, CancellationToken)"/> to avoid traversal of the buffer graph.
+ /// This method should be called prior to calling <see cref="TriggerCompletion(ITextView, CompletionTrigger, SnapshotPoint, CancellationToken)"/> to avoid traversal of the buffer graph.
/// </summary>
/// <param name="textView"><see cref="ITextView"/> to check for available completion source exports</param>
bool IsCompletionSupported(IContentType contentType);
@@ -52,20 +52,20 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// If completion was already active, returns the existing session without changing it.
/// Must be invoked on UI thread.
/// This does not cause the completion popup to appear.
- /// To compute available icons and display the UI, call <see cref="IAsyncCompletionSession.OpenOrUpdate(InitialTrigger, SnapshotPoint, CancellationToken)"/>.
+ /// To compute available icons and display the UI, call <see cref="IAsyncCompletionSession.OpenOrUpdate(CompletionTrigger, SnapshotPoint, CancellationToken)"/>.
/// Invoke <see cref="IsCompletionSupported(IContentType)"/> prior to invoking this method to more efficiently verify whether feature is disabled or if there are no completion providers.
/// </summary>
/// <param name="textView">View that hosts completion and relevant buffers</param>
+ /// <param name="trigger">What causes this completion, potentially including character typed by the user and snapshot before the text edit.</param>
/// <param name="triggerLocation">Location of completion on the view's data buffer: <see cref="ITextView.TextBuffer"/>. Used to pick relevant <see cref="IAsyncCompletionSource"/>s and <see cref="IAsyncCompletionItemManager"/></param>
- /// <param name="typeChar">Character that triggered completion, '\t', '\n' or default ('\0') </param>
/// <param name="token">Cancellation token that may interrupt this operation, despire running on the UI thread</param>
/// <returns>
/// Returns existing <see cref="IAsyncCompletionSession"/> if one already exists
/// Returns null if the completion feature is disabled or if there are no applicable completion providers. Invoke <see cref="IsCompletionSupported(IContentType)"/> prior to invoking this method to perform this check more efficiently.
/// Returns null if applicable <see cref="IAsyncCompletionSource"/>s determine that completion is not applicable at the given <paramref name="triggerLocation"/>.
- /// Returns a new <see cref="IAsyncCompletionSession"/>. Invoke <see cref="IAsyncCompletionSession.OpenOrUpdate(InitialTrigger, SnapshotPoint, CancellationToken)"/> to compute and display the available completions.
+ /// Returns a new <see cref="IAsyncCompletionSession"/>. Invoke <see cref="IAsyncCompletionSession.OpenOrUpdate(CompletionTrigger, SnapshotPoint, CancellationToken)"/> to compute and display the available completions.
/// </returns>
- IAsyncCompletionSession TriggerCompletion(ITextView textView, SnapshotPoint triggerLocation, char typedChar, CancellationToken token);
+ IAsyncCompletionSession TriggerCompletion(ITextView textView, CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token);
/// <summary>
/// Raised on UI thread when new <see cref="IAsyncCompletionSession"/> is triggered.
diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManager.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManager.cs
index c365393..0b8d285 100644
--- a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManager.cs
+++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionCommitManager.cs
@@ -18,38 +18,66 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
public interface IAsyncCompletionCommitManager
{
/// <summary>
+ /// <para>
/// Returns characters that may commit completion.
+ /// </para>
+ /// <para>
/// When completion is active and a text edit matches one of these characters,
- /// <see cref="ShouldCommitCompletion(char, SnapshotPoint, CancellationToken)"/> is called to verify that the character
+ /// <see cref="ShouldCommitCompletion(IAsyncCompletionSession, SnapshotPoint, char, CancellationToken)"/> is called to verify that the character
/// is indeed a commit character at a given location.
+ /// </para>
+ /// <para>
/// Called on UI thread.
+ /// </para>
/// </summary>
IEnumerable<char> PotentialCommitCharacters { get; }
/// <summary>
- /// Returns whether this character is a commit character in a given location.
- /// If every character returned by <see cref="PotentialCommitCharacters"/> should always commit the active completion session, return true.
+ /// <para>
+ /// Returns whether <paramref name="typedChar"/> is a commit character at a given <paramref name="location"/>.
+ /// </para>
+ /// <para>
+ /// If in your language every character returned by <see cref="PotentialCommitCharacters"/>
+ /// is a commit character, simply return <see langword="true"/>.
+ /// </para>
+ /// <para>
/// Called on UI thread.
+ /// </para>
/// </summary>
+ /// <param name="session">The active <see cref="IAsyncCompletionSession"/></param>
+ /// <param name="location">Location in the snapshot of the view's topmost buffer. The character is not inserted into this snapshot</param>
/// <param name="typedChar">Character typed by the user</param>
- /// <param name="location">Location in the snapshot of the view's topmost buffer. The character is not inserted into this snapshot.</param>
/// <param name="token">Token used to cancel this operation</param>
- /// <returns>True if this character should commit the active session.</returns>
- bool ShouldCommitCompletion(char typedChar, SnapshotPoint location, CancellationToken token);
+ /// <returns>True if this character should commit the active session</returns>
+ bool ShouldCommitCompletion(IAsyncCompletionSession session, SnapshotPoint location, char typedChar, CancellationToken token);
/// <summary>
- /// Allows the instance of <see cref="IAsyncCompletionCommitManager"/> to commit of specified <see cref="CompletionItem"/>.
- /// Implementer does not need to commit the item. Return <see cref="CommitResult.Unhandled"/> to allow another
- /// <see cref="IAsyncCompletionCommitManager"/> to attempt the commit, or to invoke default commit behavior.
+ /// <para>
+ /// Allows the implementer of <see cref="IAsyncCompletionCommitManager"/> to customize how specified <see cref="CompletionItem"/> is committed.
+ /// This method is called on UI thread, before the <paramref name="typedChar"/> is inserted into the buffer.
+ /// </para>
+ /// <para>
+ /// In most cases, implementer does not need to commit the item. Return <see cref="CommitResult.Unhandled"/> to allow another
+ /// <see cref="IAsyncCompletionCommitManager"/> to attempt the commit, or to invoke the default commit behavior.
+ /// </para>
+ /// <para>
+ /// To perform a custom commit, replace contents of <paramref name="buffer"/>
+ /// at a location indicated by <see cref="IAsyncCompletionSession.ApplicableToSpan"/>
+ /// with text stored in <see cref="CompletionItem.InsertText"/>.
+ /// To move the caret, use <see cref= "IAsyncCompletionSession.TextView" />.
+ /// Finally, return <see cref="CommitResult.Handled"/>. Use <see cref="CommitResult.Behavior"/> to influence Editor's behavior
+ /// after invoking this method.
+ /// </para>
+ /// <para>
/// Called on UI thread.
+ /// </para>
/// </summary>
- /// <param name="view">View that hosts completion and relevant buffers</param>
- /// <param name="buffer">Reference to the buffer with matching content type to perform text edits etc.</param>
- /// <param name="item">Which completion item is to be applied</param>
- /// <param name="applicableToSpan">Span augmented by completion, on the view's data buffer: <see cref="ITextView.TextBuffer"/></param>
+ /// <param name="session">The active <see cref="IAsyncCompletionSession"/>. See <see cref="IAsyncCompletionSession.ApplicableToSpan"/> and <see cref="IAsyncCompletionSession.TextView"/></param>
+ /// <param name="buffer">Subject buffer which matches this <see cref="IAsyncCompletionCommitManager"/>'s content type</param>
+ /// <param name="item">Which <see cref="CompletionItem"/> is to be committed</param>
/// <param name="typedChar">Text change associated with this commit</param>
/// <param name="token">Token used to cancel this operation</param>
/// <returns>Instruction for the editor how to proceed after invoking this method. Default is <see cref="CommitResult.Unhandled"/></returns>
- CommitResult TryCommit(ITextView view, ITextBuffer buffer, CompletionItem item, ITrackingSpan applicableToSpan, char typedChar, CancellationToken token);
+ CommitResult TryCommit(IAsyncCompletionSession session, ITextBuffer buffer, CompletionItem item, char typedChar, CancellationToken token);
}
}
diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManager.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManager.cs
index 7967e15..496fbeb 100644
--- a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManager.cs
+++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionItemManager.cs
@@ -18,8 +18,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
public interface IAsyncCompletionItemManager
{
/// <summary>
- /// This method is first called before completion is about to appear,
- /// and then on subsequent typing events and when user toggles completion filters.
+ /// This method is called before completion is about to appear,
+ /// on subsequent typing events and when user toggles completion filters.
/// <paramref name="session"/> tracks user user's input tracked with <see cref="IAsyncCompletionSession.ApplicableToSpan"/>.
/// <paramref name="data"/> provides applicable <see cref="AsyncCompletionSessionDataSnapshot.Snapshot"/> and
/// and <see cref="AsyncCompletionSessionDataSnapshot.SelectedFilters"/>s that indicate user's filter selection.
@@ -34,16 +34,15 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
CancellationToken token);
/// <summary>
- /// This method is first called before completion is about to appear,
- /// and then on subsequent typing events and when user toggles completion filters.
+ /// This method is first called before completion is about to appear.
/// The result of this method will be used in subsequent invocations of <see cref="UpdateCompletionListAsync"/>
/// <paramref name="session"/> tracks user user's input tracked with <see cref="IAsyncCompletionSession.ApplicableToSpan"/>.
/// <paramref name="data"/> provides applicable <see cref="AsyncCompletionSessionDataSnapshot.Snapshot"/> and
/// </summary>
- /// <param name="session">The active <see cref="IAsyncCompletionSession"/>. See <see cref="IAsyncCompletionSession.ApplicableToSpan"/> and <see cref="IAsyncCompletionSession.TextView"/></param>
+ /// <param name="session">The active <see cref="IAsyncCompletionSession"/>. See <see cref="IAsyncCompletionSession.TextView"/></param>
/// <param name="data">Contains properties applicable at the time this method is invoked.</param>
/// <param name="token">Cancellation token that may interrupt this operation</param>
- /// <returns>Instance of <see cref="FilteredCompletionModel"/> that contains completion items to render, filters to display and recommended item to select</returns>
+ /// <returns>Sorted <see cref="ImmutableArray"/> of <see cref="CompletionItem"/> that will be subsequently passed to <see cref="UpdateCompletionListAsync"/></returns>
Task<ImmutableArray<CompletionItem>> SortCompletionListAsync(
IAsyncCompletionSession session,
AsyncCompletionSessionInitialDataSnapshot data,
diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs
index f6549f1..e425879 100644
--- a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs
+++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSession.cs
@@ -21,7 +21,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// <param name="trigger">What caused completion</param>
/// <param name="triggerLocation">Location of the trigger on the subject buffer</param>
/// <param name="token">Token used to cancel this and other queued operation.</param>
- void OpenOrUpdate(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token);
+ void OpenOrUpdate(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token);
/// <summary>
/// Stops the session and hides associated UI.
@@ -34,7 +34,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// Since this method is on a typing hot path, it returns quickly if the <paramref name="typedChar"/>
/// is not found among characters collected from <see cref="IAsyncCompletionCommitManager.PotentialCommitCharacters"/>
/// Else, we map the top-buffer <paramref name="triggerLocation"/> to subject buffers and query
- /// <see cref="IAsyncCompletionCommitManager.ShouldCommitCompletion(char, SnapshotPoint, CancellationToken)"/>
+ /// <see cref="IAsyncCompletionCommitManager.ShouldCommitCompletion(IAsyncCompletionSession, SnapshotPoint, char, CancellationToken)"/>
/// to see whether any <see cref="IAsyncCompletionCommitManager"/> would like to commit completion.
/// Must be called on UI thread.
/// </summary>
@@ -42,7 +42,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// <param name="typedChar">The text edit which caused this action. May be null.</param>
/// <param name="triggerLocation">Location on the view's data buffer: <see cref="ITextView.TextBuffer"/></param>
/// <param name="token">Token used to cancel this operation</param>
- /// <returns>Whether any <see cref="IAsyncCompletionCommitManager.ShouldCommitCompletion(char, SnapshotPoint, CancellationToken)"/> returned true</returns>
+ /// <returns>Whether any <see cref="IAsyncCompletionCommitManager.ShouldCommitCompletion(IAsyncCompletionSession, SnapshotPoint, char, CancellationToken)"/> returned true</returns>
bool ShouldCommit(char typedChar, SnapshotPoint triggerLocation, CancellationToken token);
/// <summary>
diff --git a/src/Language/Impl/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs
index d2a0a2a..e34e945 100644
--- a/src/Language/Impl/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs
+++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSessionOperations.cs
@@ -1,14 +1,9 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Threading;
+using System.Threading;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
-namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implementation
+namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
{
/// <summary>
/// Exposes non-public functionality to commanding and tests
@@ -23,7 +18,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
/// <summary>
/// Returns whether computation has begun.
- /// Computation starts after calling <see cref="IAsyncCompletionSession.OpenOrUpdate(InitialTrigger, SnapshotPoint, CancellationToken)"/>
+ /// Computation starts after calling <see cref="IAsyncCompletionSession.OpenOrUpdate(CompletionTrigger, SnapshotPoint, CancellationToken)"/>
/// </summary>
bool IsStarted { get; }
@@ -40,7 +35,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
/// <summary>
/// Commits unique item. If no items were computed, performs computation. If there is no unique item, shows the UI.
/// </summary>
- void InvokeAndCommitIfUnique(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token);
+ void InvokeAndCommitIfUnique(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token);
/// <summary>
/// Enqueues selecting the next item. When all queued tasks are completed, the UI updates.
diff --git a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs
index fc17d0b..8917952 100644
--- a/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs
+++ b/src/Language/Def/Language/AsyncCompletion/IAsyncCompletionSource.cs
@@ -1,5 +1,4 @@
-using System.Collections.Immutable;
-using System.Threading;
+using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
using Microsoft.VisualStudio.Text;
@@ -21,12 +20,13 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// Called once per completion session to fetch the set of all completion items available at a given location.
/// Called on a background thread.
/// </summary>
+ /// <param name="session">Reference to the active <see cref="IAsyncCompletionSession"/></param>
/// <param name="trigger">What caused the completion</param>
/// <param name="triggerLocation">Location where completion was triggered, on the subject buffer that matches this <see cref="IAsyncCompletionSource"/>'s content type</param>
/// <param name="applicableToSpan">Location where completion will take place, on the view's data buffer: <see cref="ITextView.TextBuffer"/></param>
/// <param name="token">Cancellation token that may interrupt this operation</param>
/// <returns>A struct that holds completion items and applicable span</returns>
- Task<CompletionContext> GetCompletionContextAsync(InitialTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token);
+ Task<CompletionContext> GetCompletionContextAsync(IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token);
/// <summary>
/// Returns tooltip associated with provided <see cref="CompletionItem"/>.
@@ -34,27 +34,31 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// You may export a <see cref="IViewElementFactory"/> to provide a renderer for a custom type.
/// Since this method is called on a background thread and on multiple platforms, an instance of UIElement may not be returned.
/// </summary>
+ /// <param name="session">Reference to the active <see cref="IAsyncCompletionSession"/></param>
/// <param name="item"><see cref="CompletionItem"/> which is a subject of the tooltip</param>
/// <param name="token">Cancellation token that may interrupt this operation</param>
/// <returns>An object that will be passed to <see cref="IViewElementFactoryService"/>. See its documentation for supported types.</returns>
- Task<object> GetDescriptionAsync(CompletionItem item, CancellationToken token);
+ Task<object> GetDescriptionAsync(IAsyncCompletionSession session, CompletionItem item, CancellationToken token);
/// <summary>
/// Provides the span applicable to the prospective session.
- /// Called on UI thread and expected to return very quickly, based on textual information.
- /// This method is called sequentially on available <see cref="IAsyncCompletionSource"/>s until one of them returns true.
- /// Returning <code>false</code> does not exclude this source from participating in completion session.
- /// If no <see cref="IAsyncCompletionSource"/>s return <code>true</code>, there will be no completion session.
+ /// Called on UI thread and expected to return very quickly, based on syntactic clues.
+ /// This method is called as a result of user action, after the Editor makes necessary changes in direct response to user's action.
+ /// The state of the Editor prior to making the text edit is captured in <see cref="CompletionTrigger.ViewSnapshotBeforeTrigger"/> of <paramref name="trigger"/>.
+ /// This method is called sequentially on available <see cref="IAsyncCompletionSource"/>s until one of them returns
+ /// <see cref="CompletionStartData"/> with appropriate level of <see cref="CompletionStartData.Participation"/>
+ /// and one returns <see cref="CompletionStartData"/> with <see cref="CompletionStartData.ApplicableToSpan"/>
+ /// If neither of the above conditions are met, no completion session will start.
/// </summary>
/// <remarks>
- /// A language service should provide the span and return <code>true</code> even if it does not wish to provide completion.
- /// This will enable extensions to provide completion in syntactically appropriate location.
+ /// If a language service does not wish to participate in completion, it should try to provide a valid <see cref="CompletionStartData.ApplicableToSpan"/>
+ /// and set <see cref="CompletionStartData.Participation"/> to <code>false</code>.
+ /// This will enable other extensions to provide completion in syntactically appropriate location.
/// </remarks>
- /// <param name="typedChar">Character typed by the user</param>
+ /// <param name="trigger">What causes the completion, including the character typed and reference to <see cref="ITextView.TextSnapshot"/> prior to triggering the completion</param>
/// <param name="triggerLocation">Location on the subject buffer that matches this <see cref="IAsyncCompletionSource"/>'s content type</param>
- /// <param name="applicableToSpan">Applicable span for the prospective completion session. You may set it to <code>default</code> if returning false</param>
/// <param name="token">Cancellation token that may interrupt this operation</param>
- /// <returns>Whether completion should use the supplied applicable span.</returns>
- bool TryGetApplicableToSpan(char typedChar, SnapshotPoint triggerLocation, out SnapshotSpan applicableToSpan, CancellationToken token);
+ /// <returns>Whether this <see cref="IAsyncCompletionSource"/> wishes to participate in completion.</returns>
+ CompletionStartData InitializeCompletion(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token);
}
}
diff --git a/src/Language/Def/Language/AsyncCompletion/ICompletionPresenter.cs b/src/Language/Def/Language/AsyncCompletion/ICompletionPresenter.cs
index 1187224..3101212 100644
--- a/src/Language/Def/Language/AsyncCompletion/ICompletionPresenter.cs
+++ b/src/Language/Def/Language/AsyncCompletion/ICompletionPresenter.cs
@@ -16,13 +16,13 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// Opens the UI and displays provided data
/// </summary>
/// <param name="presentation">Data to display in the UI</param>
- void Open(CompletionPresentationViewModel presentation);
+ void Open(IAsyncCompletionSession session, CompletionPresentationViewModel presentation);
/// <summary>
/// Updates the UI with provided data
/// </summary>
/// <param name="presentation">Data to display in the UI</param>
- void Update(CompletionPresentationViewModel presentation);
+ void Update(IAsyncCompletionSession session, CompletionPresentationViewModel presentation);
/// <summary>
/// Hides the completion UI
@@ -35,7 +35,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
event EventHandler<CompletionFilterChangedEventArgs> FiltersChanged;
/// <summary>
- /// Notifies of user selecting an item
+ /// Notifies of user selecting an item.
+ /// When item is selected programmatically, firing this event may result in endless loop.
/// </summary>
event EventHandler<CompletionItemSelectedEventArgs> CompletionItemSelected;
diff --git a/src/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs b/src/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs
index 80debb8..17becc9 100644
--- a/src/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs
+++ b/src/Language/Def/Language/AsyncCompletion/PredefinedCompletionNames.cs
@@ -23,6 +23,11 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
public const string CompletionCommandHandler = "CompletionCommandHandler";
/// <summary>
+ /// Name of the editor option that stores user's preference for dismissing completion rather than blocking for potentially long running tasks.
+ /// </summary>
+ public const string NonBlockingCompletionOptionName = "NonBlockingCompletion";
+
+ /// <summary>
/// Name of the editor option that stores user's preference for the completion mode.
/// </summary>
public const string SuggestionModeInCompletionOptionName = "SuggestionModeInCompletion";
@@ -30,6 +35,6 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion
/// <summary>
/// Name of the editor option that stores user's preference for the completion mode during debugging.
/// </summary>
- public const string SuggestionModeInDebuggerCompletionOptionName = "SuggestionModeInCompletionDuringDebugging";
+ public const string SuggestionModeInDebuggerCompletionOptionName = "SuggestionModeInDebuggerViewCompletion";
}
}
diff --git a/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs b/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs
index 66c3976..cad8d36 100644
--- a/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs
+++ b/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionBroker.cs
@@ -112,7 +112,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
return null;
}
- public IAsyncCompletionSession TriggerCompletion(ITextView textView, SnapshotPoint triggerLocation, char typedChar, CancellationToken token)
+ public IAsyncCompletionSession TriggerCompletion(ITextView textView, CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token)
{
var session = GetSession(textView);
if (session != null)
@@ -136,12 +136,15 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
GetCommitManagersAndChars(textView.BufferGraph, textView.Roles, textView, triggerLocation, GetCommitManagerProviders, telemetry,
out var managersWithBuffers, out var potentialCommitChars);
- GetCompletionSources(textView.TextBuffer, textView.Roles, textView, textView.BufferGraph, triggerLocation, GetItemSourceProviders, telemetry, typedChar, token,
+ GetCompletionSources(textView.TextBuffer, textView.Roles, textView, textView.BufferGraph, trigger, triggerLocation, GetItemSourceProviders, telemetry, token,
out var sourcesWithLocations, out var applicableToSpan);
// No source declared an appropriate ApplicableToSpan
if (applicableToSpan == default)
return null;
+ // No source wishes to participate
+ if (!sourcesWithLocations.Any())
+ return null;
if (_contentTypeComparer == null)
_contentTypeComparer = new StableContentTypeComparer(ContentTypeRegistryService);
@@ -223,14 +226,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
private void GetCompletionSources(
ITextBuffer editBuffer,
ITextViewRoleSet roles,
- ITextView textViewForGetOrCreate, /* This name conveys that we're using ITextView only to init the MEF part. this is subject to change. */
+ ITextView textViewForGetOrCreate, /* This name conveys that we're supposed to use ITextView only to init the MEF part. this is subject to change. */
IBufferGraph bufferGraph,
+ CompletionTrigger trigger,
SnapshotPoint triggerLocation,
Func<IContentType, ITextViewRoleSet, IReadOnlyList<Lazy<IAsyncCompletionSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>>> getImports,
CompletionSessionTelemetry telemetry,
- char typedChar,
CancellationToken token,
- out IList<(IAsyncCompletionSource Source, SnapshotPoint Point)> sourcesWithLocations,
+ out List<(IAsyncCompletionSource Source, SnapshotPoint Point)> sourcesWithLocations,
out SnapshotSpan applicableToSpan)
{
var sourcesWithData = MetadataUtilities<IAsyncCompletionSourceProvider, IOrderableContentTypeAndOptionalTextViewRoleMetadata>
@@ -238,7 +241,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
var applicableToSpanBuilder = default(SnapshotSpan);
bool applicableToSpanExists = false;
- var sourcesWithLocationsBuider = new List<(IAsyncCompletionSource, SnapshotPoint)>(sourcesWithData.Count());
+ bool anySourceParticipates = false;
+ bool anySourceExclusive = false;
+ var sourcesWithLocationsBuider = new List<(IAsyncCompletionSource, SnapshotPoint, CompletionParticipation)>(sourcesWithData.Count());
foreach (var (buffer, point, import) in sourcesWithData)
{
@@ -253,25 +258,36 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
if (source == null)
continue;
- applicableToSpanExists |= GuardedOperations.CallExtensionPoint(
+ GuardedOperations.CallExtensionPoint(
errorSource: source,
call: () =>
{
- // We want to iterate through all sources and add them to collection
- sourcesWithLocationsBuider.Add((source, point));
- // Get the span only if we haven't received one yet
- if (!applicableToSpanExists)
- return source.TryGetApplicableToSpan(typedChar, point, out applicableToSpanBuilder, token);
- else
- return false; // applicableToSpanExists is already true, so it doesn't matter what we return here
- },
- valueOnThrow: false);
+ // Iterate through all sources and add them to collection
+ CompletionStartData startData;
+ startData = source.InitializeCompletion(trigger, point, token);
+
+ if (!applicableToSpanExists && startData.ApplicableToSpan != default)
+ {
+ applicableToSpanExists = true;
+ applicableToSpanBuilder = startData.ApplicableToSpan;
+ }
+ if (startData.Participation == CompletionParticipation.ProvidesItems)
+ {
+ anySourceParticipates = true;
+ }
+ else if (startData.Participation == CompletionParticipation.ExclusivelyProvidesItems)
+ {
+ anySourceParticipates = true;
+ anySourceExclusive = true;
+ }
+ sourcesWithLocationsBuider.Add((source, point, startData.Participation));
+ });
telemetry.UiStopwatch.Stop();
telemetry.RecordObtainingSourceSpan(source, telemetry.UiStopwatch.ElapsedMilliseconds);
}
- // Assume that sources are ordered. If this source is the first one to provide span, map it to the view's edit buffer and use it for completion,
+ // Map the applicable to span to the view's edit buffer and use it for completion,
if (applicableToSpanExists)
{
var mappingSpan = bufferGraph.CreateMappingSpan(applicableToSpanBuilder, SpanTrackingMode.EdgeInclusive);
@@ -279,7 +295,18 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
// Copying temporary values because we can't access out&ref params in lambdas
- sourcesWithLocations = sourcesWithLocationsBuider;
+ if (anySourceExclusive)
+ {
+ sourcesWithLocations = sourcesWithLocationsBuider.Where(n => n.Item3 == CompletionParticipation.ExclusivelyProvidesItems).Select(n => (n.Item1, n.Item2)).ToList();
+ }
+ else if (anySourceParticipates)
+ {
+ sourcesWithLocations = sourcesWithLocationsBuider.Where(n => n.Item3 == CompletionParticipation.ProvidesItems).Select(n => (n.Item1, n.Item2)).ToList();
+ }
+ else
+ {
+ sourcesWithLocations = new List<(IAsyncCompletionSource Source, SnapshotPoint Point)>();
+ }
applicableToSpan = applicableToSpanBuilder;
}
diff --git a/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs b/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs
index aad950d..6f147af 100644
--- a/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs
+++ b/src/Language/Impl/Language/AsyncCompletion/AsyncCompletionSession.cs
@@ -7,6 +7,8 @@ using System.Threading.Tasks;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Data;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Projection;
+using Microsoft.VisualStudio.Text.Utilities;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using Strings = Microsoft.VisualStudio.Language.Intellisense.Implementation.Strings;
@@ -46,18 +48,30 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
// ------------------------------------------------------------------------
// Fixed completion model data that is guaranteed not to change when another thread accesses it.
// Rare exceptions:
- // * model is Unavailable - we change ApplicableToSpan on the worker thread, but we know that UI thread won't access it
- // * session was triggered in virtual whitespace, but not updated yet. We update ApplicableToSpan, and we know that worker thread won't access it.
+ // * Session was triggered in virtual whitespace.
+ // We are in a command handler on the UI thread. We may change ApplicableToSpan until first call to OpenOrUpdate, which begins asynchronous work.
+
+ private ITrackingSpan _applicableToSpan;
+ private bool _canChangeApplicableToSpan = true;
/// <summary>
/// Span pertinent to this completion.
/// </summary>
- public ITrackingSpan ApplicableToSpan { get; set; }
+ public ITrackingSpan ApplicableToSpan
+ {
+ get => _applicableToSpan;
+ set
+ {
+ if (!_canChangeApplicableToSpan)
+ throw new InvalidOperationException($"{nameof(ApplicableToSpan)} may not be changed after completion items were received.");
+ _applicableToSpan = value ?? throw new ArgumentNullException(nameof(value));
+ }
+ }
/// <summary>
/// Stores the initial reason this session was triggererd.
/// </summary>
- private InitialTrigger InitialTrigger { get; set; }
+ private CompletionTrigger InitialTrigger { get; set; }
/// <summary>
/// Text to display in place of suggestion mode when filtered text is empty.
@@ -83,14 +97,18 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
private static SuggestionItemOptions DefaultSuggestionModeOptions = new SuggestionItemOptions(string.Empty, Strings.SuggestionModeDefaultTooltip);
- // Facilitate experience when there are no items to display
+ // Facilitate special experiences
private bool _selectionModeBeforeNoResultFallback;
+ private bool _selectionModeBeforeCaretLocationFallback;
private bool _inNoResultFallback;
+ private bool _inCaretLocationFallback;
private bool _ignoreCaretMovement;
+ private string _previouslySelectedItemText;
public event EventHandler<CompletionItemEventArgs> ItemCommitted;
public event EventHandler Dismissed;
public event EventHandler<ComputedCompletionItemsEventArgs> ItemsUpdated;
+ public event EventHandler ReceivedCompletionContext;
public ITextView TextView => _textView;
@@ -135,7 +153,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
.Where(n => n.Item2.HasValue)
.Any(n => _guardedOperations.CallExtensionPoint(
errorSource: n.Item1,
- call: () => n.Item1.ShouldCommitCompletion(typedChar, n.Item2.Value, token),
+ call: () => n.Item1.ShouldCommitCompletion(this, n.Item2.Value, typedChar, token),
valueOnThrow: false));
}
@@ -204,10 +222,32 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
if (!JoinableTaskContext.IsOnMainThread)
throw new InvalidOperationException($"This method must be callled on the UI thread.");
- _telemetry.UiStopwatch.Restart();
- var lastModel = _computation.WaitAndGetResult(cancelUi: true, token);
- _telemetry.UiStopwatch.Stop();
- _telemetry.RecordBlockingWaitForComputation(_telemetry.UiStopwatch.ElapsedMilliseconds);
+ // 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
+ && (CompletionUtilities.GetSuggestionModeOption(_textView) || CompletionUtilities.GetNonBlockingCompletionOption(_textView))
+ && !(typedChar.Equals(default) || typedChar.Equals('\t')))
+ {
+ ((IAsyncCompletionSession)this).Dismiss();
+ return CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers;
+ }
+
+ CompletionModel lastModel;
+
+ // We are in low latency mode
+ // => use recently computed model, but don't block waiting for one
+ if (CompletionUtilities.GetNonBlockingCompletionOption(_textView))
+ {
+ lastModel = _computation.RecentModel;
+ }
+ else
+ {
+ _telemetry.UiStopwatch.Restart();
+ lastModel = _computation.WaitAndGetResult(cancelUi: true, token);
+ _telemetry.UiStopwatch.Stop();
+ _telemetry.RecordBlockingWaitForComputation(_telemetry.UiStopwatch.ElapsedMilliseconds);
+ }
if (lastModel == null)
{
@@ -223,7 +263,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
{
// In soft selection mode, user commits explicitly (click, tab, e.g. not tied to a text change). Otherwise, we dismiss the session
((IAsyncCompletionSession)this).Dismiss();
- return CommitBehavior.None;
+ return CommitBehavior.RaiseFurtherReturnKeyAndTabKeyCommandHandlers;
}
else if (lastModel.SelectSuggestionItem && string.IsNullOrWhiteSpace(lastModel.SuggestionItem?.InsertText))
{
@@ -262,7 +302,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
{
var commitResult = _guardedOperations.CallExtensionPoint(
errorSource: commitManager,
- call: () => commitManager.Item1.TryCommit(_textView, commitManager.Item2 /* buffer */, itemToCommit, applicableToSpan, typedChar, token),
+ call: () => commitManager.Item1.TryCommit(this, commitManager.Item2 /* buffer */, itemToCommit, typedChar, token),
valueOnThrow: CommitResult.Unhandled);
if (commitResult.Behavior == CommitBehavior.CancelCommit)
@@ -301,11 +341,18 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
private static void InsertIntoBuffer(ITextView view, ITrackingSpan applicableToSpan, string insertText)
{
var buffer = view.TextBuffer;
- var bufferEdit = buffer.CreateEdit();
+ var replacedSpan = applicableToSpan.GetSpan(buffer.CurrentSnapshot);
- // ApplicableToSpan already contains the typedChar and brace completion. Replacing this span will cause us to lose this data.
- // The command handler who invoked this code needs to re-play the type char command, such that we get these changes back.
- bufferEdit.Replace(applicableToSpan.GetSpan(buffer.CurrentSnapshot), insertText);
+ // If edit would be effectively a no-op, leave early so we don't create a no-op undo operation
+ var replacedText = replacedSpan.GetText();
+ if (insertText.Equals(replacedText, StringComparison.Ordinal))
+ return;
+
+ // If the commit is a result of typing a commit character, the handler of TypeCharCommandArgs must replay typing:
+ // At this instant, ApplicableToSpan already contains the typed char and braces added by the Brace Completion feature.
+ // Replacing this span will forfeit the braces. Therefore, the typing must be replayed so that the matching brace is inserted.
+ var bufferEdit = buffer.CreateEdit();
+ bufferEdit.Replace(replacedSpan, insertText);
bufferEdit.Apply();
}
@@ -336,6 +383,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
copyOfGui.Close();
_telemetry.UiStopwatch.Stop();
_telemetry.RecordClosing(_telemetry.UiStopwatch.ElapsedMilliseconds);
+
await Task.Yield();
_telemetry.Save(_completionItemManager, _presenterProvider);
});
@@ -343,7 +391,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
}
- void IAsyncCompletionSession.OpenOrUpdate(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken commandToken)
+ void IAsyncCompletionSession.OpenOrUpdate(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken commandToken)
{
if (IsDismissed)
return;
@@ -353,12 +401,26 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
commandToken.Register(_computationCancellation.Cancel);
+ var rootSnapshot = TextView.TextSnapshot;
+ if (TextView.Properties.ContainsProperty("CompletionRoot"))
+ {
+ // In certain scenarios, TextView.TextSnapshot is not the appropriate snapshot to use.
+ // For example, in the watch window (C# debugger), TextView.TextSnapshot corresponds to the single line for the expression.
+ // Roslyn uses the property bag to indicate the correct snapshot to use.
+ if (TextView.Properties.TryGetProperty("CompletionRoot", out ITextBuffer rootBuffer))
+ {
+ rootSnapshot = rootBuffer.CurrentSnapshot;
+ }
+ }
+
+ _canChangeApplicableToSpan = false; // Don't allow changing the ApplicableToSpan from now on.
+
if (_computation == null)
{
_computation = new ModelComputation<CompletionModel>(
PrioritizedTaskScheduler.AboveNormalInstance,
JoinableTaskContext,
- (model, token) => GetInitialModel(trigger, triggerLocation, token),
+ (model, token) => GetInitialModel(trigger, triggerLocation, rootSnapshot, token),
_computationCancellation.Token,
_guardedOperations,
this
@@ -366,7 +428,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
var taskId = Interlocked.Increment(ref _lastFilteringTaskId);
- _computation.Enqueue((model, token) => UpdateSnapshot(model, trigger, new UpdateTrigger(FromCompletionTriggerReason(trigger.Reason), trigger.Character), triggerLocation, taskId, token), updateUi: true);
+ _computation.Enqueue((model, token) => UpdateSnapshot(model, trigger, triggerLocation, rootSnapshot, taskId, token), updateUi: true);
}
ComputedCompletionItems IAsyncCompletionSession.GetComputedItems(CancellationToken token)
@@ -378,25 +440,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
return ComputeCompletionItems(model);
}
- private static UpdateTriggerReason FromCompletionTriggerReason(InitialTriggerReason reason)
- {
- switch (reason)
- {
- case InitialTriggerReason.Invoke:
- case InitialTriggerReason.InvokeAndCommitIfUnique:
- return UpdateTriggerReason.Initial;
- case InitialTriggerReason.Insertion:
- return UpdateTriggerReason.Insertion;
- case InitialTriggerReason.Deletion:
- return UpdateTriggerReason.Deletion;
- default:
- throw new ArgumentOutOfRangeException(nameof(reason));
- }
- }
-
#region IAsyncCompletionSessionOperations implementation
- public void InvokeAndCommitIfUnique(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token)
+ public void InvokeAndCommitIfUnique(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token)
{
if (IsDismissed)
return;
@@ -492,8 +538,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
private void OnItemSelected(object sender, CompletionItemSelectedEventArgs args)
{
// Note 1: Use this only to react to selection changes initiated by user's mouse\touch operation in the UI, since they cancel the soft selection
- // Note 2: we are not enqueuing a call to update the UI, since this would put us in infinite loop, and the UI is already updated
- _computation.Enqueue((model, token) => UpdateSelectedItem(model, args.SelectedItem, args.SuggestionItemSelected, token), updateUi: false);
+ _computation.Enqueue((model, token) => UpdateSelectedItem(model, args.SelectedItem, args.SuggestionItemSelected, token), updateUi: true);
}
private void OnGuiClosed(object sender, CompletionClosedEventArgs args)
@@ -502,10 +547,9 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
/// <summary>
- /// Monitors when user scrolled outside of the applicable span. Note that:
- /// * This event is not raised during regular typing.
- /// * This event is raised by brace completion.
- /// * Typing stretches the applicable span
+ /// Monitors when user scrolled outside of the applicable span.
+ /// Note that this event is NOT raised during regular typing.
+ /// It is raised by brace completion, but at the same time we set _ignoreCaretMovement.
/// </summary>
private void OnCaretPositionChanged(object sender, CaretPositionChangedEventArgs e)
{
@@ -551,13 +595,13 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
call: () =>
{
_gui = _presenterProvider.GetOrCreate(_textView);
- _gui.Open(new CompletionPresentationViewModel(model.PresentedItems, model.Filters,
- model.SelectedIndex, ApplicableToSpan, model.UseSoftSelection, model.DisplaySuggestionItem,
- model.SelectSuggestionItem, model.SuggestionItem, SuggestionItemOptions));
_gui.FiltersChanged += OnFiltersChanged;
_gui.CommitRequested += OnCommitRequested;
_gui.CompletionItemSelected += OnItemSelected;
_gui.CompletionClosed += OnGuiClosed;
+ _gui.Open(this, new CompletionPresentationViewModel(model.PresentedItems, model.Filters,
+ model.SelectedIndex, ApplicableToSpan, model.UseSoftSelection, model.DisplaySuggestionItem,
+ model.SelectSuggestionItem, model.SuggestionItem, SuggestionItemOptions));
});
}
}
@@ -565,7 +609,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
{
_guardedOperations.CallExtensionPoint(
errorSource: _gui,
- call: () => _gui.Update(new CompletionPresentationViewModel(model.PresentedItems, model.Filters,
+ call: () => _gui.Update(this, new CompletionPresentationViewModel(model.PresentedItems, model.Filters,
model.SelectedIndex, ApplicableToSpan, model.UseSoftSelection, model.DisplaySuggestionItem,
model.SelectSuggestionItem, model.SuggestionItem, SuggestionItemOptions)));
}
@@ -576,7 +620,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
/// <summary>
/// Creates a new model and populates it with initial data
/// </summary>
- private async Task<CompletionModel> GetInitialModel(InitialTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token)
+ private async Task<CompletionModel> GetInitialModel(CompletionTrigger trigger, SnapshotPoint triggerLocation, ITextSnapshot rootSnapshot, CancellationToken token)
{
bool sourceUsesSuggestionMode = false;
SuggestionItemOptions requestedSuggestionItemOptions = null;
@@ -590,7 +634,20 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
_telemetry.ComputationStopwatch.Restart();
var context = await _guardedOperations.CallExtensionPointAsync(
errorSource: _completionSources[index].Source,
- asyncCall: () => _completionSources[index].Source.GetCompletionContextAsync(trigger, _completionSources[index].Point, ApplicableToSpan.GetSpan(ApplicableToSpan.TextBuffer.CurrentSnapshot), token),
+ asyncCall: () =>
+ {
+ var mappingPoint = MappingPointSnapshot.Create(
+ root: rootSnapshot,
+ anchor: triggerLocation,
+ trackingMode: PointTrackingMode.Positive,
+ graph: TextView.BufferGraph);
+
+ var triggerLocationOnSubjectBuffer = mappingPoint.GetPoint(_completionSources[index].Point.Snapshot.TextBuffer, PositionAffinity.Predecessor);
+
+ if (!triggerLocationOnSubjectBuffer.HasValue)
+ return null;
+ return _completionSources[index].Source.GetCompletionContextAsync(this, trigger, triggerLocationOnSubjectBuffer.Value, ApplicableToSpan.GetSpan(triggerLocation.Snapshot), token);
+ },
valueOnThrow: null
).ConfigureAwait(true);
_telemetry.ComputationStopwatch.Stop();
@@ -617,6 +674,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
{
return CompletionModel.GetUninitializedModel(triggerLocation.Snapshot);
}
+ ReceivedCompletionContext?.Invoke(this, EventArgs.Empty);
// If no source provided suggestion item options, provide default options for suggestion mode
SuggestionItemOptions = requestedSuggestionItemOptions ?? DefaultSuggestionModeOptions;
@@ -633,10 +691,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
.Select(n => new CompletionFilterWithState(n, true))
.ToImmutableArray();
- var customerUsesSuggestionMode = CompletionUtilities.GetSuggestionModeOption(_textView);
- var viewUsesSuggestionMode = CompletionUtilities.IsDebuggerTextView(_textView);
-
- var useSuggestionMode = customerUsesSuggestionMode || sourceUsesSuggestionMode || viewUsesSuggestionMode;
+ var viewUsesSuggestionMode = CompletionUtilities.GetSuggestionModeOption(_textView);
+ var useSuggestionMode = sourceUsesSuggestionMode || viewUsesSuggestionMode;
// Select suggestion item only if source explicity provided it. This means that debugger view or ctrl+alt+space won't select the suggestion item.
var selectSuggestionItem = sourceUsesSuggestionMode;
// Use soft selection if suggestion item is present, unless source selects that item. Also, use soft selection if source wants to.
@@ -663,12 +719,46 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
/// </summary>
private void HandleCaretPositionChanged(CaretPosition caretPosition)
{
- // TODO: when caret goes to the beginning of the span, we should enter soft selection
- // when caret moves back into another location in the span, we should resume previous selection mode.
- if (!ApplicableToSpan.GetSpan(caretPosition.VirtualBufferPosition.Position.Snapshot).IntersectsWith(new SnapshotSpan(caretPosition.VirtualBufferPosition.Position, 0)))
+ var currentTaskId = _lastFilteringTaskId;
+ _computation?.Enqueue((model, token) => UpdateCaretPosition(model, caretPosition, currentTaskId, token), updateUi: true);
+ }
+
+#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+ private async Task<CompletionModel> UpdateCaretPosition(CompletionModel model, CaretPosition caretPosition, int taskId, CancellationToken token)
+#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
+ {
+ if (token.IsCancellationRequested || model == null)
+ return default;
+
+ var caretSnapshot = caretPosition.BufferPosition.Snapshot;
+ var immediateCaretPosition = caretPosition.BufferPosition;
+ var immediateSpanStart = ApplicableToSpan.GetStartPoint(caretSnapshot);
+ CompletionModel updatedModel = model;
+ if (!ApplicableToSpan.GetSpan(caretSnapshot).IntersectsWith(new SnapshotSpan(immediateCaretPosition, 0)))
{
- ((IAsyncCompletionSession)this).Dismiss();
+ // Caret is outside of the applicable to span
+ Dismiss();
+ }
+ else if (immediateCaretPosition == immediateSpanStart && !_inCaretLocationFallback)
+ {
+ // Caret is at the beginning of the applicable to span; enter the special soft selection mode
+ _selectionModeBeforeCaretLocationFallback = model.UseSoftSelection;
+ updatedModel = model.WithSoftSelection(true);
+ _inCaretLocationFallback = true;
+
+ if (taskId == _lastFilteringTaskId)
+ RaiseCompletionItemsComputedEvent(updatedModel);
}
+ else if (immediateCaretPosition != immediateSpanStart && _inCaretLocationFallback)
+ {
+ // Caret is within the applicable to span; leave the special soft selection mode
+ updatedModel = model.WithSoftSelection(_selectionModeBeforeCaretLocationFallback);
+ _inCaretLocationFallback = false;
+
+ if (taskId == _lastFilteringTaskId)
+ RaiseCompletionItemsComputedEvent(updatedModel);
+ }
+ return updatedModel;
}
/// <summary>
@@ -686,7 +776,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
/// <summary>
/// User has typed. Update the known snapshot, filter the items and update the model.
/// </summary>
- private async Task<CompletionModel> UpdateSnapshot(CompletionModel model, InitialTrigger initialTrigger, UpdateTrigger updateTrigger, SnapshotPoint updateLocation, int thisId, CancellationToken token)
+ private async Task<CompletionModel> UpdateSnapshot(CompletionModel model, CompletionTrigger trigger, SnapshotPoint updateLocation, ITextSnapshot rootSnapshot, int thisId, CancellationToken token)
{
// Always record keystrokes, even if filtering is preempted
_telemetry.RecordKeystroke();
@@ -695,57 +785,65 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
if (token.IsCancellationRequested || model == null)
return default;
- var instantenousSnapshot = updateLocation.Snapshot;
+ var instantaneousSnapshot = updateLocation.Snapshot;
// Dismiss if we are outside of the applicable span
- var currentlyApplicableToSpan = ApplicableToSpan.GetSpan(instantenousSnapshot);
+ var currentlyApplicableToSpan = ApplicableToSpan.GetSpan(instantaneousSnapshot);
if (updateLocation < currentlyApplicableToSpan.Start
|| updateLocation > currentlyApplicableToSpan.End)
{
((IAsyncCompletionSession)this).Dismiss();
return model;
}
- // Record the first time the span is empty. If it is empty the second time we're here, and user is deleting, then dismiss
- if (currentlyApplicableToSpan.IsEmpty && model.ApplicableToSpanWasEmpty && initialTrigger.Reason == InitialTriggerReason.Deletion)
+ // If the applicable to span was empty, is empty again, and user is deleting, then dismiss
+ if (currentlyApplicableToSpan.IsEmpty
+ && model.ApplicableToSpanWasEmpty
+ && (trigger.Reason == CompletionTriggerReason.Deletion || trigger.Reason == CompletionTriggerReason.Backspace))
{
((IAsyncCompletionSession)this).Dismiss();
return model;
}
- // If we were soft selected at the beginning of the span
+ // If user is backspacing at the beginning of a span, dismiss
+ if (updateLocation == currentlyApplicableToSpan.Start && trigger.Reason == CompletionTriggerReason.Backspace)
+ {
+ if (_inCaretLocationFallback)
+ {
+ // If user was previously at the beginning of the span, this backspace will dismiss completion
+ ((IAsyncCompletionSession)this).Dismiss();
+ return model;
+ }
+ else
+ {
+ // Caret just moved to the beginning of the span, enter soft selection
+ _selectionModeBeforeCaretLocationFallback = model.UseSoftSelection;
+ model = model.WithSoftSelection(true);
+ _inCaretLocationFallback = true;
+ }
+ }
+
+ // Record whether the applicable to span is empty
model = model.WithApplicableToSpanStatus(currentlyApplicableToSpan.IsEmpty);
- // The model has no items. There is a chance that there will be items available
- // after user types something. Due to timing issues, we can't just dismiss and start another session,
- // so we need to attempt to get items again within this session.
+ // The model previously received no items, but we are called again because user typed something.
+ // There is a chance that language service will provide items this time.
+ // Due to timing issues, if we dismiss and start another session, we would miss some user actions.
+ // Instead, attempt to get items again within this session.
if (model.Uninitialized && thisId > 1) // Don't attempt to get items on the very first UpdateSnapshot
{
- // previous ApplicableToSpan returned no items.
- // When we try getting items again, use a span that doesn't have characters present in the previous span
- // Update the applicable span to the new snapshot, without the span that previously did not return any items
- var previousSpan = ApplicableToSpan.GetSpan(model.Snapshot);
- var pointThatDoesntTrackAdditions = model.Snapshot.CreateTrackingPoint(previousSpan.End, PointTrackingMode.Negative);
- var newSpan = ApplicableToSpan.GetSpan(updateLocation.Snapshot);
-
- var newApplicableToSpanStart = pointThatDoesntTrackAdditions.GetPosition(updateLocation.Snapshot);
- var newApplicableToSpanEnd = newSpan.End;
-
- var newApplicableToSpan = updateLocation.Snapshot.CreateTrackingSpan(newApplicableToSpanStart, newApplicableToSpanEnd - newApplicableToSpanStart, SpanTrackingMode.EdgeInclusive);
-
- this.ApplicableToSpan = newApplicableToSpan; // Everyone expects this to not change, but we are confident that the UI thread is waiting for this method to complete.
// Attempt to get new completion items
- model = await GetInitialModel(initialTrigger, updateLocation, token).ConfigureAwait(true);
+ model = await GetInitialModel(trigger, updateLocation, rootSnapshot, token).ConfigureAwait(true);
}
- if (model.Uninitialized) // Check if we just received some items
+ // If we still have no items, dismiss, unless there is another task queued (because user has typed).
+ if (model.Uninitialized)
{
- // If not, dismiss, unless there is another task queued.
var dismissed = await TryDismissSafely(thisId).ConfigureAwait(true);
return model;
}
- // Filtering got preempted, so store the most recent snapshot for the next time we filter. UpdateSnapshot will be called again.
+ // There is another taks queued: We are preempted, store the most recent snapshot for the upcoming invocation of UpdateSnapshot
if (thisId != _lastFilteringTaskId)
- return model.WithSnapshot(instantenousSnapshot);
+ return model.WithSnapshot(instantaneousSnapshot);
_telemetry.ComputationStopwatch.Restart();
@@ -755,9 +853,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
session: this,
data: new AsyncCompletionSessionDataSnapshot(
model.InitialItems,
- instantenousSnapshot,
- initialTrigger,
- updateTrigger,
+ instantaneousSnapshot,
+ trigger,
model.Filters,
model.UseSoftSelection,
model.DisplaySuggestionItem),
@@ -771,9 +868,20 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
return model;
}
- // Special experience when there are no more selected items:
- ImmutableArray<CompletionItemWithHighlight> returnedItems;
+ // Other error cases that we attribute to the IAsyncCompletionItemManager
+ if (filteredCompletion.SelectedItemIndex == -1 && !model.DisplaySuggestionItem)
+ {
+ _guardedOperations.HandleException(errorSource: _completionItemManager,
+ e: new InvalidOperationException($"{nameof(IAsyncCompletionItemManager)} recommends selecting suggestion item when there is no suggestion item."));
+ ((IAsyncCompletionSession)this).Dismiss();
+ return model;
+ }
+
int selectedIndex = filteredCompletion.SelectedItemIndex;
+ bool selectedIndexOverridden = false; // Used when ApplicableToSpan is empty
+
+ // Special experience when there are no returned items:
+ ImmutableArray<CompletionItemWithHighlight> returnedItems;
if (filteredCompletion.Items.IsDefault)
{
// Prevent null references when service returns default(ImmutableArray)
@@ -802,13 +910,43 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
else
{
+ // Default behavior, we received completion items
+ returnedItems = filteredCompletion.Items;
+
if (_inNoResultFallback)
{
// we were in the no result mode and just received no items. Restore the selection mode.
model = model.WithSoftSelection(_selectionModeBeforeNoResultFallback);
_inNoResultFallback = false;
}
- returnedItems = filteredCompletion.Items;
+
+ // Special experience when ApplicableToSpan is empty: attempt to select last selected item
+ if (currentlyApplicableToSpan.IsEmpty && !string.IsNullOrEmpty(_previouslySelectedItemText))
+ {
+ int indexOfPreviouslySelectedItem = -1;
+ for (int i = 0; i < filteredCompletion.Items.Length; i++)
+ {
+ if (filteredCompletion.Items[i].CompletionItem.DisplayText.Equals(_previouslySelectedItemText, StringComparison.Ordinal))
+ {
+ indexOfPreviouslySelectedItem = i;
+ break;
+ }
+ }
+ if (indexOfPreviouslySelectedItem != -1)
+ {
+ // We found a matching item
+ model = model.WithSelectedIndex(indexOfPreviouslySelectedItem, preserveSoftSelection: true).WithSoftSelection(true);
+ selectedIndexOverridden = true;
+ }
+ }
+
+ // Leave the caret location fallback if user just typed something
+ if (_inCaretLocationFallback && trigger.Reason == CompletionTriggerReason.Insertion)
+ {
+ // User just typed something, so we can't be at the beginning of applicable to span. Revert the selection mode.
+ model = model.WithSoftSelection(_selectionModeBeforeCaretLocationFallback);
+ _inCaretLocationFallback = false;
+ }
}
_telemetry.ComputationStopwatch.Stop();
@@ -819,24 +957,24 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
{
if (filteredCompletion.SelectedItemIndex == -1)
model = model.WithSuggestionItemSelected();
- else
+ else if (!selectedIndexOverridden)
model = model.WithSelectedIndex(selectedIndex, preserveSoftSelection: true);
// If suggestion item is present, we default to soft selection.
model = model.WithSoftSelection(true);
+
+ _previouslySelectedItemText = string.Empty;
}
- else
+ else if (!selectedIndexOverridden && !returnedItems.IsDefaultOrEmpty)
{
model = model.WithSelectedIndex(selectedIndex, preserveSoftSelection: true);
+ _previouslySelectedItemText = returnedItems[selectedIndex].CompletionItem.DisplayText;
}
// Allow the item manager to override the selection style.
// Our recommendation for extenders is to use UpdateSelectionHint.NoChange whenever possible
if (filteredCompletion.SelectionHint == UpdateSelectionHint.SoftSelected)
model = model.WithSoftSelection(true);
- else if (filteredCompletion.SelectionHint == UpdateSelectionHint.Selected
- && (!model.DisplaySuggestionItem || model.SelectSuggestionItem))
- // Allow the language service wishes to fully select the item if we are not in suggestion mode,
- // or if the item to select is the suggestion item.
+ else if (filteredCompletion.SelectionHint == UpdateSelectionHint.Selected)
model = model.WithSoftSelection(false);
// Prepare the suggestionItem if user ever activates suggestion mode
@@ -880,7 +1018,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
_telemetry.RecordChangingFilters();
_telemetry.RecordKeystroke();
- // Filtering got preempted, so store the most updated filters for the next time we filter
+ // This operation 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);
@@ -891,8 +1029,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
data: new AsyncCompletionSessionDataSnapshot(
model.InitialItems,
model.Snapshot,
- InitialTrigger,
- new UpdateTrigger(UpdateTriggerReason.FilterChange),
+ new CompletionTrigger(CompletionTriggerReason.FilterChange, model.Snapshot),
newFilters,
model.UseSoftSelection,
model.DisplaySuggestionItem),
@@ -943,7 +1080,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
if (offset > 0) // Scrolling down. Stop at last index and don't wrap around.
{
if (currentIndex == lastIndex)
- return model;
+ return model.WithSoftSelection(false); // Don't wrap around, but ensure that this item is fully selected
var newIndex = currentIndex + offset;
return model.WithSelectedIndex(Math.Min(newIndex, lastIndex));
@@ -952,14 +1089,14 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
{
if (currentIndex < FirstIndex) // Suggestion mode item is selected.
{
- return model; // Don't wrap around.
+ return model.WithSoftSelection(false); // Don't wrap around, but ensure that this item is fully selected
}
else if (currentIndex == FirstIndex) // The first item is selected.
{
if (model.DisplaySuggestionItem) // If there is a suggestion, select it.
return model.WithSuggestionItemSelected();
else
- return model; // Don't wrap around.
+ return model.WithSoftSelection(false); // Don't wrap around, but ensure that this item is fully selected
}
var newIndex = currentIndex + offset;
return model.WithSelectedIndex(Math.Max(newIndex, FirstIndex));
@@ -1002,11 +1139,24 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
{
if (ItemsUpdated == null)
return;
+ ThreadPool.QueueUserWorkItem(new WaitCallback(RaiseCompletionItemsComputedEventOnBackground), model);
+ }
+
+ private void RaiseCompletionItemsComputedEventOnBackground(object parameter)
+ {
+ if (IsDismissed)
+ return;
+ var handlers = ItemsUpdated;
+ if (handlers == null)
+ return;
+ if (!(parameter is CompletionModel model))
+ return;
var computedItems = ComputeCompletionItems(model);
// Warning: if the event handler throws and anyone blocks UI thread now, there will be a deadlock.
// This won't happen for now, because all callers of this method are private and nobody waits on them.
+
_guardedOperations.RaiseEvent(this, ItemsUpdated, new ComputedCompletionItemsEventArgs(computedItems));
}
diff --git a/src/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs b/src/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs
index e0599f1..27b58b4 100644
--- a/src/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs
+++ b/src/Language/Impl/Language/AsyncCompletion/CompletionCommandHandlers.cs
@@ -20,34 +20,36 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
[TextViewRole(PredefinedTextViewRoles.Interactive)]
[Export(typeof(ICommandHandler))]
internal sealed class CompletionCommandHandler :
- ICommandHandler<DownKeyCommandArgs>,
- ICommandHandler<PageDownKeyCommandArgs>,
- ICommandHandler<PageUpKeyCommandArgs>,
- ICommandHandler<UpKeyCommandArgs>,
IChainedCommandHandler<BackspaceKeyCommandArgs>,
IDynamicCommandHandler<BackspaceKeyCommandArgs>,
- ICommandHandler<EscapeKeyCommandArgs>,
- IDynamicCommandHandler<EscapeKeyCommandArgs>,
- ICommandHandler<InvokeCompletionListCommandArgs>,
ICommandHandler<CommitUniqueCompletionListItemCommandArgs>,
- ICommandHandler<InsertSnippetCommandArgs>,
- ICommandHandler<SurroundWithCommandArgs>,
- ICommandHandler<ToggleCompletionModeCommandArgs>,
+ ICommandHandler<CutCommandArgs>,
IChainedCommandHandler<DeleteKeyCommandArgs>,
IDynamicCommandHandler<DeleteKeyCommandArgs>,
- ICommandHandler<WordDeleteToEndCommandArgs>,
- ICommandHandler<WordDeleteToStartCommandArgs>,
- ICommandHandler<SaveCommandArgs>,
- ICommandHandler<SelectAllCommandArgs>,
- ICommandHandler<RenameCommandArgs>,
- ICommandHandler<UndoCommandArgs>,
+ ICommandHandler<DownKeyCommandArgs>,
+ ICommandHandler<EscapeKeyCommandArgs>,
+ IDynamicCommandHandler<EscapeKeyCommandArgs>,
+ ICommandHandler<InsertSnippetCommandArgs>,
+ ICommandHandler<InvokeCompletionListCommandArgs>,
+ ICommandHandler<PageDownKeyCommandArgs>,
+ ICommandHandler<PageUpKeyCommandArgs>,
+ ICommandHandler<PasteCommandArgs>,
ICommandHandler<RedoCommandArgs>,
+ ICommandHandler<RenameCommandArgs>,
IChainedCommandHandler<ReturnKeyCommandArgs>,
IDynamicCommandHandler<ReturnKeyCommandArgs>,
+ ICommandHandler<SaveCommandArgs>,
+ ICommandHandler<SelectAllCommandArgs>,
+ ICommandHandler<SurroundWithCommandArgs>,
IChainedCommandHandler<TabKeyCommandArgs>,
IDynamicCommandHandler<TabKeyCommandArgs>,
+ ICommandHandler<ToggleCompletionModeCommandArgs>,
IChainedCommandHandler<TypeCharCommandArgs>,
- IDynamicCommandHandler<TypeCharCommandArgs>
+ IDynamicCommandHandler<TypeCharCommandArgs>,
+ ICommandHandler<UndoCommandArgs>,
+ ICommandHandler<UpKeyCommandArgs>,
+ ICommandHandler<WordDeleteToEndCommandArgs>,
+ ICommandHandler<WordDeleteToStartCommandArgs>
{
[Import]
private IAsyncCompletionBroker Broker;
@@ -65,7 +67,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
/// <summary>
/// Helper method that returns command state for commands
- /// that are always available - unless the completion feature is available.
+ /// which are available as long as the completion feature is available.
/// </summary>
private CommandState GetCommandStateIfCompletionIsAvailable(IContentType contentType, ITextView textView)
{
@@ -75,13 +77,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
/// <summary>
- /// Helper method that returns command state
- /// for commands that are available IF AND ONLY IF completion is active,
+ /// Helper method that returns command state for commands
+ /// which are available IF AND ONLY IF completion is active,
/// even if the commands would be otherwise unavailable.
/// </summary>
- /// <remarks>
- /// For commands whose availability is not influenced by completion, use <see cref="CommandState.Unspecified"/>
- /// </remarks>
private CommandState GetCommandStateIfCompletionIsActive(ITextView textView)
{
return Broker.IsCompletionActive(textView)
@@ -90,8 +89,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
/// <summary>
- /// Helper method that returns command state for commands that are available when completion is
- /// either currently active, or available.
+ /// Helper method that returns command state for commands
+ /// which are available when completion is either currently active, or available.
/// This is used by commands that may trigger completion session on a specified buffer, or interact with an active completion session on another buffer
/// </summary>
private CommandState GetCommandStateIfCompletionIsActiveOrAvailable(IContentType contentType, ITextView textView)
@@ -108,14 +107,13 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
private CommandState GetCommandStateForSuggestionModeToggle(IContentType contentType, ITextView textView)
{
var isAvailable = CompletionAvailability.IsAvailable(contentType, textView);
- var isChecked = CompletionUtilities.IsDebuggerTextView(textView)
- ? CompletionUtilities.GetSuggestionModeOption(textView)
- : CompletionUtilities.GetSuggestionModeInDebuggerCompletionOption(textView);
+ var isChecked = CompletionUtilities.GetSuggestionModeOption(textView);
return new CommandState(isAvailable, isChecked);
}
/// <summary>
- /// Realizes the virtual space and updates session's applicable to span
+ /// Realizes the virtual space and updates session's applicable to span.
+ /// We invoke this method after the session has triggered, because we don't want to act if there would be no completion.
/// </summary>
private void RealizeVirtualSpaceUpdateApplicableToSpan(IAsyncCompletionSessionOperations session, ITextView textView)
{
@@ -131,33 +129,41 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
editorOperations?.InsertText("");
// ApplicableToSpan just grew to include the realized white space.
- // We know that ApplicableToSpan was zero length, so let's recreate a zero length span at the caret location
+ // We know that ApplicableToSpan was zero length, so let's recreate a zero length span at the caret location.
+ // This method executed synchronously, and therefore we know that it is safe to modify the applicable to span.
session.ApplicableToSpan = textView.TextSnapshot.CreateTrackingSpan(
start: textView.Caret.Position.BufferPosition.Position,
length: 0,
- trackingMode: SpanTrackingMode.EdgePositive);
+ trackingMode: SpanTrackingMode.EdgeInclusive);
}
// ----- Command handlers:
CommandState IChainedCommandHandler<BackspaceKeyCommandArgs>.GetCommandState(BackspaceKeyCommandArgs args, Func<CommandState> nextCommandHandler)
- => CommandState.Unspecified;
+ => nextCommandHandler();
bool IDynamicCommandHandler<BackspaceKeyCommandArgs>.CanExecuteCommand(BackspaceKeyCommandArgs args)
- => Broker.IsCompletionActive(args.TextView);
+ => Broker.IsCompletionActive(args.TextView) || Broker.IsCompletionSupported(args.SubjectBuffer.ContentType);
void IChainedCommandHandler<BackspaceKeyCommandArgs>.ExecuteCommand(BackspaceKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
{
+ var snapshotBeforeEdit = args.TextView.TextSnapshot;
// Execute other commands in the chain to see the change in the buffer.
nextCommandHandler();
var session = Broker.GetSession(args.TextView);
+ var location = args.TextView.Caret.Position.BufferPosition;
+ var trigger = new CompletionTrigger(CompletionTriggerReason.Backspace, snapshotBeforeEdit);
+
if (session != null)
{
- var trigger = new InitialTrigger(InitialTriggerReason.Deletion);
- var location = args.TextView.Caret.Position.BufferPosition;
session.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
}
+ else
+ {
+ var newSession = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken);
+ newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
+ }
}
CommandState ICommandHandler<EscapeKeyCommandArgs>.GetCommandState(EscapeKeyCommandArgs args)
@@ -185,14 +191,15 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
if (!GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView).IsAvailable)
return false;
- var trigger = new InitialTrigger(InitialTriggerReason.Invoke);
+ var trigger = new CompletionTrigger(CompletionTriggerReason.Invoke, args.TextView.TextSnapshot);
var location = args.TextView.Caret.Position.BufferPosition;
- var session = Broker.TriggerCompletion(args.TextView, location, default, executionContext.OperationContext.UserCancellationToken);
+ var session = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken);
+
if (session is IAsyncCompletionSessionOperations sessionInternal)
{
RealizeVirtualSpaceUpdateApplicableToSpan(sessionInternal, args.TextView);
location = args.TextView.Caret.Position.BufferPosition; // Buffer may have changed. Update the location.
- sessionInternal.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
+ session.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
return true;
}
return false;
@@ -206,9 +213,11 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
if (!GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView).IsAvailable)
return false;
- var trigger = new InitialTrigger(InitialTriggerReason.InvokeAndCommitIfUnique);
+ var snapshotBeforeEdit = args.TextView.TextSnapshot;
+ var trigger = new CompletionTrigger(CompletionTriggerReason.InvokeAndCommitIfUnique, args.TextView.TextSnapshot);
var location = args.TextView.Caret.Position.BufferPosition;
- var session = Broker.TriggerCompletion(args.TextView, location, default, executionContext.OperationContext.UserCancellationToken);
+ var session = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken);
+
if (session is IAsyncCompletionSessionOperations sessionInternal)
{
RealizeVirtualSpaceUpdateApplicableToSpan(sessionInternal, args.TextView);
@@ -254,23 +263,30 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
CommandState IChainedCommandHandler<DeleteKeyCommandArgs>.GetCommandState(DeleteKeyCommandArgs args, Func<CommandState> nextCommandHandler)
- => CommandState.Unspecified;
+ => nextCommandHandler();
bool IDynamicCommandHandler<DeleteKeyCommandArgs>.CanExecuteCommand(DeleteKeyCommandArgs args)
- => Broker.IsCompletionActive(args.TextView);
+ => Broker.IsCompletionActive(args.TextView) || Broker.IsCompletionSupported(args.SubjectBuffer.ContentType);
void IChainedCommandHandler<DeleteKeyCommandArgs>.ExecuteCommand(DeleteKeyCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext)
{
+ var snapshotBeforeEdit = args.TextView.TextSnapshot;
// Execute other commands in the chain to see the change in the buffer.
nextCommandHandler();
var session = Broker.GetSession(args.TextView);
+ var location = args.TextView.Caret.Position.BufferPosition;
+ var trigger = new CompletionTrigger(CompletionTriggerReason.Deletion, snapshotBeforeEdit);
+
if (session != null)
{
- var trigger = new InitialTrigger(InitialTriggerReason.Deletion);
- var location = args.TextView.Caret.Position.BufferPosition;
session.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
}
+ else
+ {
+ var newSession = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken);
+ newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
+ }
}
CommandState ICommandHandler<WordDeleteToEndCommandArgs>.GetCommandState(WordDeleteToEndCommandArgs args)
@@ -336,8 +352,27 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
return false;
}
+ CommandState ICommandHandler<CutCommandArgs>.GetCommandState(CutCommandArgs args)
+ => CommandState.Unspecified;
+
+ bool ICommandHandler<CutCommandArgs>.ExecuteCommand(CutCommandArgs args, CommandExecutionContext executionContext)
+ {
+ Broker.GetSession(args.TextView)?.Dismiss();
+ return false;
+ }
+
+ CommandState ICommandHandler<PasteCommandArgs>.GetCommandState(PasteCommandArgs args)
+ => CommandState.Unspecified;
+
+ bool ICommandHandler<PasteCommandArgs>.ExecuteCommand(PasteCommandArgs args, CommandExecutionContext executionContext)
+ {
+ Broker.GetSession(args.TextView)?.Dismiss();
+ return false;
+ }
+
CommandState IChainedCommandHandler<ReturnKeyCommandArgs>.GetCommandState(ReturnKeyCommandArgs args, Func<CommandState> nextCommandHandler)
- => GetCommandStateIfCompletionIsActiveOrAvailable(args.SubjectBuffer.ContentType, args.TextView);
+ => nextCommandHandler();
+
bool IDynamicCommandHandler<ReturnKeyCommandArgs>.CanExecuteCommand(ReturnKeyCommandArgs args)
=> Broker.IsCompletionActive(args.TextView) || Broker.IsCompletionSupported(args.SubjectBuffer.ContentType);
@@ -365,23 +400,24 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
return;
}
+ var snapshotBeforeEdit = args.TextView.TextSnapshot;
nextCommandHandler();
// Buffer has changed. Update it for when we try to trigger new session.
var location = args.TextView.Caret.Position.BufferPosition;
- var trigger = new InitialTrigger(InitialTriggerReason.Insertion, typedChar);
- var newSession = Broker.TriggerCompletion(args.TextView, location, typedChar, executionContext.OperationContext.UserCancellationToken);
+ var trigger = new CompletionTrigger(CompletionTriggerReason.Insertion, snapshotBeforeEdit, typedChar);
+ var newSession = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken);
if (newSession is IAsyncCompletionSessionOperations sessionInternal)
{
RealizeVirtualSpaceUpdateApplicableToSpan(sessionInternal, args.TextView);
- location = args.TextView.Caret.Position.BufferPosition; // Buffer may have changed. Update the location.
- sessionInternal.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
}
+ location = args.TextView.Caret.Position.BufferPosition; // Buffer may have changed. Update the location.
+ newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
}
CommandState IChainedCommandHandler<TabKeyCommandArgs>.GetCommandState(TabKeyCommandArgs args, Func<CommandState> nextCommandHandler)
- => GetCommandStateIfCompletionIsActiveOrAvailable(args.SubjectBuffer.ContentType, args.TextView);
+ => nextCommandHandler();
bool IDynamicCommandHandler<TabKeyCommandArgs>.CanExecuteCommand(TabKeyCommandArgs args)
=> Broker.IsCompletionActive(args.TextView) || Broker.IsCompletionSupported(args.SubjectBuffer.ContentType);
@@ -408,19 +444,19 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
|| CompletionUtilities.IsDebuggerTextView(args.TextView))
return;
}
-
+ var snapshotBeforeEdit = args.TextView.TextSnapshot;
nextCommandHandler();
// Buffer has changed. Update it for when we try to trigger new session.
var location = args.TextView.Caret.Position.BufferPosition;
- var trigger = new InitialTrigger(InitialTriggerReason.Insertion, typedChar);
- var newSession = Broker.TriggerCompletion(args.TextView, location, typedChar, executionContext.OperationContext.UserCancellationToken);
+ var trigger = new CompletionTrigger(CompletionTriggerReason.Insertion, snapshotBeforeEdit, typedChar);
+ var newSession = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken);
newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
}
CommandState IChainedCommandHandler<TypeCharCommandArgs>.GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextCommandHandler)
- => GetCommandStateIfCompletionIsAvailable(args.SubjectBuffer.ContentType, args.TextView);
+ => nextCommandHandler();
bool IDynamicCommandHandler<TypeCharCommandArgs>.CanExecuteCommand(TypeCharCommandArgs args)
=> CompletionAvailability.IsAvailable(args.SubjectBuffer.ContentType, args.TextView);
@@ -453,6 +489,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
((AsyncCompletionSession)sessionToCommit).IgnoreCaretMovement(ignore: true);
}
+ 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
nextCommandHandler();
@@ -494,7 +531,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
// Buffer might have changed. Update it for when we try to trigger new session.
location = view.Caret.Position.BufferPosition;
- var trigger = new InitialTrigger(InitialTriggerReason.Insertion, args.TypedChar);
+ var trigger = new CompletionTrigger(CompletionTriggerReason.Insertion, snapshotBeforeEdit, args.TypedChar);
var session = Broker.GetSession(args.TextView);
if (session != null)
{
@@ -502,7 +539,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
else
{
- var newSession = Broker.TriggerCompletion(args.TextView, location, args.TypedChar, executionContext.OperationContext.UserCancellationToken);
+ var newSession = Broker.TriggerCompletion(args.TextView, trigger, location, executionContext.OperationContext.UserCancellationToken);
newSession?.OpenOrUpdate(trigger, location, executionContext.OperationContext.UserCancellationToken);
}
}
@@ -554,10 +591,8 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
if (Broker.GetSession(args.TextView) is AsyncCompletionSession session) // we are accessing an internal method
{
session.SelectUp();
- System.Diagnostics.Debug.WriteLine("Completions's UpKey command handler returns true (handled)");
return true;
}
- System.Diagnostics.Debug.WriteLine("Completions's UpKey command handler returns false (unhandled)");
return false;
}
}
diff --git a/src/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs b/src/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs
index 6392376..bf95dfe 100644
--- a/src/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs
+++ b/src/Language/Impl/Language/AsyncCompletion/CompletionUtilities.cs
@@ -34,8 +34,10 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
/// <returns>True if the view has "DEBUGVIEW" text view role.</returns>
internal static bool IsDebuggerTextView(ITextView textView) => textView.Roles.Contains("DEBUGVIEW");
+ static readonly EditorOptionKey<bool> 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;
@@ -61,32 +63,50 @@ 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;
- if (!(options.IsOptionDefined(SuggestionModeOptionKey, localScopeOnly: false)))
- options.SetOptionValue(SuggestionModeOptionKey, UseSuggestionModeDefaultValue);
- return options.GetOptionValue(SuggestionModeOptionKey);
+ var optionKey = IsDebuggerTextView(textView) ? SuggestionModeInDebuggerCompletionOptionKey : SuggestionModeOptionKey;
+ if (!(options.IsOptionDefined(optionKey, localScopeOnly: false)))
+ {
+ var defaultValue = IsDebuggerTextView(textView) ? UseSuggestionModeInDebuggerCompletionDefaultValue : UseSuggestionModeDefaultValue;
+ options.SetOptionValue(optionKey, defaultValue);
+ }
+ return options.GetOptionValue(optionKey);
}
internal static void SetSuggestionModeOption(ITextView textView, bool value)
{
var options = textView.Options.GlobalOptions;
- options.SetOptionValue(SuggestionModeOptionKey, value);
+ var optionKey = IsDebuggerTextView(textView) ? SuggestionModeInDebuggerCompletionOptionKey : SuggestionModeOptionKey;
+ options.SetOptionValue(optionKey, value);
}
- internal static bool GetSuggestionModeInDebuggerCompletionOption(ITextView textView)
+ internal static bool GetNonBlockingCompletionOption(ITextView textView)
{
var options = textView.Options.GlobalOptions;
- if (!(options.IsOptionDefined(SuggestionModeInDebuggerCompletionOptionKey, localScopeOnly: false)))
- options.SetOptionValue(SuggestionModeInDebuggerCompletionOptionKey, UseSuggestionModeInDebuggerCompletionDefaultValue);
- return options.GetOptionValue(SuggestionModeInDebuggerCompletionOptionKey);
+ if (!(options.IsOptionDefined(NonBlockingCompletionOptionKey, localScopeOnly: false)))
+ {
+ options.SetOptionValue(NonBlockingCompletionOptionKey, NonBlockingCompletionDefaultValue);
+ }
+ return options.GetOptionValue(NonBlockingCompletionOptionKey);
}
- internal static void SetSuggestionModeDuringDebuggingOption(ITextView textView, bool value)
+ internal static void SetNonBlockingModeOption(ITextView textView, bool value)
{
var options = textView.Options.GlobalOptions;
- options.SetOptionValue(SuggestionModeInDebuggerCompletionOptionKey, value);
+ options.SetOptionValue(NonBlockingCompletionOptionKey, value);
}
}
}
diff --git a/src/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs b/src/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs
index 9ae95b5..8a4ab8b 100644
--- a/src/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs
+++ b/src/Language/Impl/Language/AsyncCompletion/DefaultCompletionItemManager.cs
@@ -85,7 +85,35 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
}
var bestMatch = filterFilteredList.OrderByDescending(n => n.Item2.HasValue).ThenBy(n => n.Item2).FirstOrDefault();
- var listWithHighlights = filterFilteredList.Select(n => n.Item2.HasValue ? new CompletionItemWithHighlight(n.completionItem, n.Item2.Value.MatchedSpans) : new CompletionItemWithHighlight(n.completionItem)).ToImmutableArray();
+ var listWithHighlights = filterFilteredList.Select(n =>
+ {
+ ImmutableArray<Span> safeMatchedSpans = ImmutableArray<Span>.Empty;
+ if (n.completionItem.DisplayText.Equals(n.completionItem.FilterText, StringComparison.Ordinal))
+ {
+ if (n.Item2.HasValue)
+ {
+ safeMatchedSpans = n.Item2.Value.MatchedSpans;
+ }
+ }
+ else
+ {
+ // Matches were made against FilterText. We are displaying DisplayText. To avoid issues, re-apply matches for these items
+ var newMatchedSpans = patternMatcher.TryMatch(n.completionItem.DisplayText);
+ if (newMatchedSpans.HasValue)
+ {
+ safeMatchedSpans = newMatchedSpans.Value.MatchedSpans;
+ }
+ }
+
+ if (safeMatchedSpans.IsDefaultOrEmpty)
+ {
+ return new CompletionItemWithHighlight(n.completionItem);
+ }
+ else
+ {
+ return new CompletionItemWithHighlight(n.completionItem, safeMatchedSpans);
+ }
+ }).ToImmutableArray();
int selectedItemIndex = 0;
if (data.DisplaySuggestionItem)
diff --git a/src/Language/Impl/Language/AsyncCompletion/ModelComputation.cs b/src/Language/Impl/Language/AsyncCompletion/ModelComputation.cs
index d553d3e..67dbacb 100644
--- a/src/Language/Impl/Language/AsyncCompletion/ModelComputation.cs
+++ b/src/Language/Impl/Language/AsyncCompletion/ModelComputation.cs
@@ -83,11 +83,12 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
try
{
var previousModel = await previousTask;
- // Previous task finished processing. We are ready to execute next piece of work.
+
if (_token.IsCancellationRequested || _terminated)
return previousModel;
- var transformedModel = await transformation(await previousTask, _token).ConfigureAwait(true);
+ // Previous task finished processing. We are ready to execute next piece of work.
+ var transformedModel = await transformation(previousModel, _token).ConfigureAwait(true);
RecentModel = transformedModel;
// TODO: update UI even if updateUi is false but it wasn't updated yet.
@@ -97,15 +98,19 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
if (!_uiCancellation.IsCancellationRequested)
_callbacks.UpdateUI(transformedModel, _uiCancellation.Token).Forget();
}
-
return transformedModel;
}
catch (Exception ex)
{
+ // Disallow enqueuing more tasks
_terminated = true;
- _guardedOperations.HandleException(this, ex);
+ // Log the issue
+ if (!(ex is ThreadAbortException))
+ _guardedOperations.HandleException(this, ex);
+ // Close completion
_callbacks.Dismiss();
- return await previousTask;
+ // Return a task that has not faulted
+ return default(TModel);
}
});
diff --git a/src/Language/Impl/Language/AsyncCompletion/PrioritizedTaskScheduler.cs b/src/Language/Impl/Language/AsyncCompletion/PrioritizedTaskScheduler.cs
index 3c054d9..bd210ef 100644
--- a/src/Language/Impl/Language/AsyncCompletion/PrioritizedTaskScheduler.cs
+++ b/src/Language/Impl/Language/AsyncCompletion/PrioritizedTaskScheduler.cs
@@ -43,16 +43,13 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
_tasks.Add(task);
}
- // A class derived from TaskScheduler implements this function to support inline execution
- // of a task on a thread that initiates a wait on that task object.
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
- // NOTE(cyrusn): There is no race condition here. While our dedicated thread may try to
- // call "TryExecuteTask" on this task above *and* we allow another "Wait"ing thread to
- // execute it, the TPL ensures that only one will ever get a go. And, since we have no
- // ordering guarantees (or other constraints) we're happy to let some other thread try
- // to execute this task. It means less work for us, and it makes that other thread not
- // be blocked.
+ if (Thread.CurrentThread != _thread)
+ {
+ // Don't allow tasks to execute on other threads.
+ return false;
+ }
return this.TryExecuteTask(task);
}
diff --git a/src/Language/Impl/Language/AsyncCompletion/SuggestionModeCompletionItemSource.cs b/src/Language/Impl/Language/AsyncCompletion/SuggestionModeCompletionItemSource.cs
index 97d031b..c33c5d0 100644
--- a/src/Language/Impl/Language/AsyncCompletion/SuggestionModeCompletionItemSource.cs
+++ b/src/Language/Impl/Language/AsyncCompletion/SuggestionModeCompletionItemSource.cs
@@ -20,20 +20,19 @@ namespace Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion.Implement
_options = options ?? throw new ArgumentNullException(nameof(options));
}
- Task<CompletionContext> IAsyncCompletionSource.GetCompletionContextAsync(InitialTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token)
+ Task<CompletionContext> IAsyncCompletionSource.GetCompletionContextAsync(IAsyncCompletionSession session, CompletionTrigger trigger, SnapshotPoint triggerLocation, SnapshotSpan applicableToSpan, CancellationToken token)
{
throw new NotImplementedException("This item source is not meant to be registered. It is used only to provide a tooltip.");
}
- Task<object> IAsyncCompletionSource.GetDescriptionAsync(CompletionItem item, CancellationToken token)
+ Task<object> IAsyncCompletionSource.GetDescriptionAsync(IAsyncCompletionSession session, CompletionItem item, CancellationToken token)
{
return Task.FromResult<object>(_options.ToolTipText);
}
- bool IAsyncCompletionSource.TryGetApplicableToSpan(char typedChar, SnapshotPoint triggerLocation, out SnapshotSpan applicableToSpan, CancellationToken token)
+ public CompletionStartData InitializeCompletion(CompletionTrigger trigger, SnapshotPoint triggerLocation, CancellationToken token)
{
- applicableToSpan = default;
- return false;
+ return CompletionStartData.DoesNotParticipateInCompletion;
}
}
}
diff --git a/src/Language/Impl/Language/Strings.Designer.cs b/src/Language/Impl/Language/Strings.Designer.cs
index d1a0428..ff61a5c 100644
--- a/src/Language/Impl/Language/Strings.Designer.cs
+++ b/src/Language/Impl/Language/Strings.Designer.cs
@@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Strings {
@@ -61,6 +61,15 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation {
}
/// <summary>
+ /// Looks up a localized string similar to Executing code cleanup.
+ /// </summary>
+ public static string CodeCleanupDescription {
+ get {
+ return ResourceManager.GetString("CodeCleanupDescription", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Completion command handler.
/// </summary>
public static string CompletionCommandHandlerName {
@@ -70,7 +79,7 @@ namespace Microsoft.VisualStudio.Language.Intellisense.Implementation {
}
/// <summary>
- /// Looks up a localized string similar to Suggestion mode. Allows typing delimeters without auto completing..
+ /// Looks up a localized string similar to Suggestion mode item which allows you to provide a name not present in the completion list..
/// </summary>
public static string SuggestionModeDefaultTooltip {
get {
diff --git a/src/Language/Impl/Language/Strings.resx b/src/Language/Impl/Language/Strings.resx
index c391c70..f7f343d 100644
--- a/src/Language/Impl/Language/Strings.resx
+++ b/src/Language/Impl/Language/Strings.resx
@@ -121,7 +121,7 @@
<value>Completion command handler</value>
</data>
<data name="SuggestionModeDefaultTooltip" xml:space="preserve">
- <value>Suggestion mode. Allows typing delimeters without auto completing.</value>
- <comment>Tooltip on suggestion mode completion item (visible when suggestion mode is enabled, through Ctrl+Alt+Space)</comment>
+ <value>Suggestion mode item which allows you to provide a name not present in the completion list.</value>
+ <comment>Tooltip on the suggestion mode completion item (visible when suggestion mode is enabled, through Ctrl+Alt+Space)</comment>
</data>
</root> \ No newline at end of file
diff --git a/src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs b/src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs
new file mode 100644
index 0000000..ddf8714
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+// This file contain internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Support the scenario where we want to create the editor options and then create the corresponding view (so that the editor
+ /// options can be seeded with options the view will want).
+ /// </summary>
+ public interface IEditorOptionsFactoryService2 : IEditorOptionsFactoryService
+ {
+ /// <summary>
+ /// Create a new <see cref="IEditorOptions"/> that is not bound to a particular scope.
+ /// </summary>
+ /// <param name="allowLateBinding">If true, this option can be bound to a scope after it has been created using <see cref="TryBindToScope(IEditorOptions, IPropertyOwner)"/>.</param>
+ /// <returns></returns>
+ IEditorOptions CreateOptions(bool allowLateBinding);
+
+
+ /// <summary>
+ /// Binds <paramref name="option"/> to the specified scope if the scope does not have pre-existing <see cref="IEditorOptions"/> and <paramref name="option"/> was
+ /// created using <see cref="CreateOptions(bool)"/> with the late binding allowed.
+ /// </summary>
+ /// <returns>true if <paramref name="option"/> was bound to <paramref name="scope"/>.</returns>
+ bool TryBindToScope(IEditorOptions option, IPropertyOwner scope);
+
+ /// <summary>
+ /// Get the option definition associated with <paramref name="optionId"/>.
+ /// </summary>
+ /// <param name="optionId"></param>
+ /// <returns></returns>
+ EditorOptionDefinition GetOptionDefinition(string optionId);
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs
index dad1d94..936da15 100644
--- a/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs
+++ b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs
@@ -61,12 +61,15 @@ namespace Microsoft.VisualStudio.Text.Utilities
/// Changing this will force the event to send to Watson. Be careful because it can have big perf impact.
/// If unchanged, it will be set according to the default sample rate.
/// </param>
+ /// <param name="correlations">TelemetryEventCorrelations which help correlate this fault to the scope it was executing within</param>
void PostFault(
string eventName,
string description,
Exception exceptionObject,
- string additionalErrorInfo,
- bool? isIncludedInWatsonSample);
+ string additionalErrorInfo = null,
+ bool? isIncludedInWatsonSample = null,
+ object[] correlations = null
+ );
/// <summary>
/// Adjust the counter associated with <paramref name="key"/> and <paramref name="name"/> by <paramref name="delta"/>.
@@ -85,5 +88,9 @@ namespace Microsoft.VisualStudio.Text.Utilities
/// <para>The counters are cleared as a side-effect of this call.</para>
/// </remarks>
void PostCounters();
+
+ object CreateTelemetryOperationEventScope(string eventName, TelemetrySeverity severity, object[] correlations, IDictionary<string, object> startingProperties);
+ object GetCorrelationFromTelemetryScope(object telemetryScope);
+ void EndTelemetryScope(object telemetryScope, TelemetryResult result, string summary = null);
}
}
diff --git a/src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs b/src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs
new file mode 100644
index 0000000..11a0080
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs
@@ -0,0 +1,20 @@
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.OptionDescriptions
+{
+ /// <summary>
+ /// Attribute defining whether or not the description has an associated option.
+ /// </summary>
+ /// <remarks>
+ /// Defaults to true. Set to false for static elements in the options page.
+ /// </remarks>
+ public sealed class IsOptionAttribute : SingletonBaseMetadataAttribute
+ {
+ public IsOptionAttribute(bool isOption)
+ {
+ this.IsOption = isOption;
+ }
+
+ public bool IsOption { get; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs b/src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs
new file mode 100644
index 0000000..f681510
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs
@@ -0,0 +1,20 @@
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.OptionDescriptions
+{
+ /// <summary>
+ /// Attribute defining whether or not the option roams.
+ /// </summary>
+ /// <remarks>
+ /// If not provided, then the option is not considered a roaming attribute.
+ /// </remarks>
+ public sealed class IsRoamingAttribute : SingletonBaseMetadataAttribute
+ {
+ public IsRoamingAttribute(bool isRoaming = true)
+ {
+ this.IsRoaming = isRoaming;
+ }
+
+ public bool IsRoaming { get; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ParentAttribute.cs b/src/Text/Def/Internal/TextLogic/ParentAttribute.cs
new file mode 100644
index 0000000..9951024
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ParentAttribute.cs
@@ -0,0 +1,20 @@
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.OptionDescriptions
+{
+ /// <summary>
+ /// Attribute indicating the parent option/page of the option.
+ /// </summary>
+ /// <remarks>
+ /// Options can be nested or attached directly to the their corresponding page.
+ /// </remarks>
+ public sealed class ParentAttribute : SingletonBaseMetadataAttribute
+ {
+ public ParentAttribute(string parent)
+ {
+ this.Parent = parent;
+ }
+
+ public string Parent { get; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs b/src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs
new file mode 100644
index 0000000..7bbf5e2
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs
@@ -0,0 +1,22 @@
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ //
+ // Summary:
+ // An enum to define the severity of the telemetry event. It is used for any data
+ // consumer who wants to categorize data based on severity.
+ public enum TelemetrySeverity : int
+ {
+ //
+ // Summary:
+ // indicates telemetry event with verbose information.
+ Low = -10,
+ //
+ // Summary:
+ // indicates a regular telemetry event.
+ Normal = 0,
+ //
+ // Summary:
+ // indicates telemetry event with high value or require attention (e.g., fault).
+ High = 10
+ }
+}
diff --git a/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs b/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs
index d1e7af1..996e758 100644
--- a/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs
+++ b/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs
@@ -4,6 +4,7 @@
//
using System;
using System.ComponentModel.Composition;
+using System.Threading;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods
@@ -204,6 +205,50 @@ namespace Microsoft.VisualStudio.Text.Editor
public static readonly EditorOptionKey<string> TooltipAppearanceCategoryOptionId = new EditorOptionKey<string>(TooltipAppearanceCategoryOptionName);
public const string TooltipAppearanceCategoryOptionName = "TooltipAppearanceCategory";
+ /// <summary>
+ /// The default option that determines whether files, when opened, attempt to detect for a utf-8 encoding.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> AutoDetectUtf8Id = new EditorOptionKey<bool>(AutoDetectUtf8Name);
+ public const string AutoDetectUtf8Name = "AutoDetectUtf8";
+
+ /// <summary>
+ /// The default option that determines whether matching delimiters should be highlighted.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> AutomaticDelimiterHighlightingId = new EditorOptionKey<bool>(AutomaticDelimiterHighlightingName);
+ public const string AutomaticDelimiterHighlightingName = "AutomaticDelimiterHighlighting";
+
+ /// <summary>
+ /// The default option that determines whether files should follow project coding conventions.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> FollowCodingConventionsId = new EditorOptionKey<bool>(FollowCodingConventionsName);
+ public const string FollowCodingConventionsName = "FollowCodingConventions";
+
+ /// <summary>
+ /// The default option that determines the editor emulation mode.
+ /// </summary>
+ public static readonly EditorOptionKey<int> EditorEmulationModeId = new EditorOptionKey<int>(EditorEmulationModeName);
+ public const string EditorEmulationModeName = "EditorEmulationMode";
+
+ /// <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
+ /// Tools/Options/Text Editor/Advanced page.
+ /// </summary>
+ internal static readonly EditorOptionKey<int> MaximumTypingLatencyOptionId = new EditorOptionKey<int>(MaximumTypingLatencyOptionName);
+ internal const string MaximumTypingLatencyOptionName = "MaximumTypingLatency";
+
+ /// <summary>
+ /// The option definition that determines user custom maximum allowed typing latency value in milliseconds. If user
+ /// specifies it on Tools/Options/Text Editor/Advanced page, it becomes a source for the <see cref="MaximumTypingLatency"/> option.
+ /// </summary>
+ internal static readonly EditorOptionKey<int> UserCustomMaximumTypingLatencyOptionId = new EditorOptionKey<int>(UserCustomMaximumTypingLatencyOptionName);
+ internal const string UserCustomMaximumTypingLatencyOptionName = "UserCustomMaximumTypingLatency";
+
+ /// <summary>
+ /// The option definition that determines whether to enable typing latency guarding.
+ /// </summary>
+ internal static readonly EditorOptionKey<bool> EnableTypingLatencyGuardOptionId = new EditorOptionKey<bool>(EnableTypingLatencyGuardOptionName);
+ internal const string EnableTypingLatencyGuardOptionName = "EnableTypingLatencyGuard";
#endregion
}
@@ -408,5 +453,102 @@ namespace Microsoft.VisualStudio.Text.Editor
public override EditorOptionKey<string> Key { get { return DefaultOptions.TooltipAppearanceCategoryOptionId; } }
}
+ /// <summary>
+ /// The option definition that determines whether files, when opened, attempt to detect for a utf-8 encoding.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.AutoDetectUtf8Name)]
+ public sealed class AutoDetectUtf8Option : EditorOptionDefinition<bool>
+ {
+ public override bool Default { get => true; }
+
+ public override EditorOptionKey<bool> Key { get { return DefaultOptions.AutoDetectUtf8Id; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines whether matching delimiters should be highlighted.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.AutomaticDelimiterHighlightingName)]
+ public sealed class AutomaticDelimiterHighlightingOption : EditorOptionDefinition<bool>
+ {
+ public override bool Default { get => true; }
+
+ public override EditorOptionKey<bool> Key { get { return DefaultOptions.AutomaticDelimiterHighlightingId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines whether files should follow project coding conventions.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.FollowCodingConventionsName)]
+ public sealed class FollowCodingConventionsOption : EditorOptionDefinition<bool>
+ {
+ public override bool Default { get => true; }
+
+ public override EditorOptionKey<bool> Key { get { return DefaultOptions.FollowCodingConventionsId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines the editor emulation mode.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.EditorEmulationModeName)]
+ public sealed class EditorEmulationModeOption : EditorOptionDefinition<int>
+ {
+ public override int Default { get => 0; }
+
+ public override EditorOptionKey<int> Key { get { return DefaultOptions.EditorEmulationModeId; } }
+ }
+
+ /// <summary>
+ ///The option definition that determines whether to enable typing latency guarding.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.EnableTypingLatencyGuardOptionName)]
+ internal sealed class EnableTypingLatencyGuard : EditorOptionDefinition<bool>
+ {
+ /// <summary>
+ /// Gets the default value (true).
+ /// </summary>
+ public override bool Default { get => true; }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<bool> Key { get { return DefaultOptions.EnableTypingLatencyGuardOptionId; } }
+ }
+
+ /// <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
+ /// Tools/Options/Text Editor/Advanced page.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.MaximumTypingLatencyOptionName)]
+ internal sealed class MaximumTypingLatency : EditorOptionDefinition<int>
+ {
+ /// <summary>
+ /// Gets the default value (infinite).
+ /// </summary>
+ public override int Default { get => Timeout.Infinite; }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<int> Key { get { return DefaultOptions.MaximumTypingLatencyOptionId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines user custom maximum allowed typing latency value in milliseconds. If user
+ /// specifies it on Tools/Options/Text Editor/Advanced page, it becomes a source for the <see cref="MaximumTypingLatency"/> option.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.UserCustomMaximumTypingLatencyOptionName)]
+ internal sealed class UserCustomMaximumTypingLatencyOption : EditorOptionDefinition<int>
+ {
+ public override int Default { get { return Timeout.Infinite; } }
+ public override EditorOptionKey<int> Key { get { return DefaultOptions.UserCustomMaximumTypingLatencyOptionId; } }
+ }
#endregion
}
diff --git a/src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs b/src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs
index 290f256..d4b0ba3 100644
--- a/src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs
+++ b/src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs
@@ -34,6 +34,13 @@ namespace Microsoft.VisualStudio.Text.Differencing
/// <remarks>This option is ignored in the other view modes.</remarks>
public static readonly EditorOptionKey<bool> SynchronizeSideBySideViewsId = new EditorOptionKey<bool>(DifferenceViewerOptions.SynchronizeSideBySideViewsName);
public const string SynchronizeSideBySideViewsName = "Diff/View/SynchronizeSideBySideViews";
+
+
+ /// <summary>
+ /// If <c>true</c>, show the difference overview margin.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> ShowDiffOverviewMarginId = new EditorOptionKey<bool>(DifferenceViewerOptions.ShowDiffOverviewMarginName);
+ public const string ShowDiffOverviewMarginName = "Diff/View/ShowDiffOverviewMargin";
}
/// <summary>
diff --git a/src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs b/src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs
new file mode 100644
index 0000000..3b9a4a8
--- /dev/null
+++ b/src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Differencing
+{
+ using System;
+
+ public interface IDifferenceViewer2 : IDifferenceViewer
+ {
+ /// <summary>
+ /// Raised when the difference viewer is fully initialized.
+ /// </summary>
+ event EventHandler Initialized;
+ }
+}
diff --git a/src/Text/Def/TextUI/Editor/ITextView2.cs b/src/Text/Def/TextUI/Editor/ITextView2.cs
index ed797ee..0469f75 100644
--- a/src/Text/Def/TextUI/Editor/ITextView2.cs
+++ b/src/Text/Def/TextUI/Editor/ITextView2.cs
@@ -4,6 +4,7 @@
//
using System;
+using Microsoft.VisualStudio.Text.Formatting;
namespace Microsoft.VisualStudio.Text.Editor
{
@@ -33,7 +34,6 @@ namespace Microsoft.VisualStudio.Text.Editor
get;
}
-
/// <summary>
/// Raised whenever the view's MaxTextRightCoordinate is changed.
/// </summary>
@@ -42,5 +42,54 @@ namespace Microsoft.VisualStudio.Text.Editor
/// (it will not be raised as a side-effect of a layout even if the layout does change the MaxTextRightCoordinate).
/// </remarks>
event EventHandler MaxTextRightCoordinateChanged;
+
+ /// <summary>
+ /// Adds an action to be performed after any layouts are complete. If there is not a layout in progress, the action will
+ /// be performed immediately. This must be called on the UI thread, and actions will be performed on the UI thread.
+ /// </summary>
+ /// <param name="action">The action to be performed.</param>
+ void QueuePostLayoutAction(Action action);
+
+ /// <summary>
+ /// Attempts to get a read-only list of the <see cref="ITextViewLine"/> objects rendered in this view.
+ /// </summary>
+ /// <remarks>
+ /// This list will be dense. That is, all characters between the first character of the first <see cref="ITextViewLine"/> through
+ /// the last character of the last <see cref="ITextViewLine"/> will be represented in one of the <see cref="ITextViewLine"/> objects,
+ /// except when the layout of the <see cref="ITextViewLine"/> objects is in progress.
+ /// <para>
+ /// <see cref="ITextViewLine"/> objects are disjoint. That is, a given character is part of only one <see cref="ITextViewLine"/>.
+ /// </para>
+ /// <para>
+ /// The <see cref="ITextViewLine"/> objects are sorted by the index of their first character.
+ /// </para>
+ /// <para>Some of the <see cref="ITextViewLine"/> objects may not be visible,
+ /// and all <see cref="ITextViewLine"/> objects will be disposed of when the view
+ /// recomputes its layout.</para>
+ /// <para>This list is occasionally not available due to layouts or other events, and callers should be prepared to handle
+ /// a failure.</para>
+ /// </remarks>
+ /// <param name="textViewLines">Returns out the <see cref="ITextViewLineCollection"/> requested.</param>
+ /// <returns>True if succeeded, false otherwise.</returns>
+ bool TryGetTextViewLines(out ITextViewLineCollection textViewLines);
+
+ /// <summary>
+ /// Attempts to get the <see cref="ITextViewLine"/> that contains the specified text buffer position.
+ /// </summary>
+ /// <param name="bufferPosition">
+ /// The text buffer position used to search for a text line.
+ /// </param>
+ /// <returns>
+ /// True if succeeded, false otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>This method returns an <see cref="ITextViewLine"/> if it exists in the view.</para>
+ /// <para>If the line does not exist in the cache of formatted lines, it will be formatted and added to the cache.</para>
+ /// <para>The returned <see cref="ITextViewLine"/> could be invalidated by either a layout by the view or by subsequent calls to this method.</para>
+ /// <para>It is occasionally invalid to retrieve an <see cref="ITextViewLine"/> due to layouts or other events. Callers should be prepared to handle
+ /// a failure.</para>
+ /// </remarks>
+ /// <param name="textViewLine">Returns out the <see cref="ITextViewLine"/> requested.</param>
+ bool TryGetTextViewLineContainingBufferPosition(SnapshotPoint bufferPosition, out ITextViewLine textViewLine);
}
}
diff --git a/src/Text/Def/TextUI/Editor/TextViewExtensions.cs b/src/Text/Def/TextUI/Editor/TextViewExtensions.cs
index ae2cfab..18354f1 100644
--- a/src/Text/Def/TextUI/Editor/TextViewExtensions.cs
+++ b/src/Text/Def/TextUI/Editor/TextViewExtensions.cs
@@ -3,6 +3,8 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
//
using System;
+using System.Diagnostics;
+using Microsoft.VisualStudio.Text.Formatting;
namespace Microsoft.VisualStudio.Text.Editor
{
@@ -84,7 +86,74 @@ namespace Microsoft.VisualStudio.Text.Editor
throw new ArgumentNullException(nameof(textView));
}
- return ((ITextView2)textView).MultiSelectionBroker;
+ if (textView is ITextView2 textView2)
+ {
+ return textView2.MultiSelectionBroker;
+ }
+
+ if (textView.Properties.TryGetProperty(typeof(IMultiSelectionBroker), out IMultiSelectionBroker broker))
+ {
+ return broker;
+ }
+
+ Debug.Fail("Failed to acquire IMultiSelectionBroker for a text view");
+
+ return null;
+ }
+
+ /// <summary>
+ /// See <see cref="ITextView2.QueuePostLayoutAction(Action)"/>.
+ /// </summary>
+ public static void QueuePostLayoutAction(this ITextView textView, Action action)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException(nameof(textView));
+ }
+
+ (textView as ITextView2)?.QueuePostLayoutAction(action);
+ }
+
+ /// <summary>
+ /// See <see cref="ITextView2.TryGetTextViewLines(out ITextViewLineCollection)"/>.
+ /// </summary>
+ public static bool TryGetTextViewLines(this ITextView textView, out ITextViewLineCollection textViewLines)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException(nameof(textView));
+ }
+
+ if (textView is ITextView2 textView2)
+ {
+ return textView2.TryGetTextViewLines(out textViewLines);
+ }
+ else
+ {
+ textViewLines = null;
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// See <see cref="ITextView2.TryGetTextViewLineContainingBufferPosition(SnapshotPoint, out Formatting.ITextViewLine)"/>.
+ /// </summary>
+ public static bool TryGetTextViewLineContainingBufferPosition(this ITextView textView, SnapshotPoint bufferPosition, out ITextViewLine textViewLine)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException(nameof(textView));
+ }
+
+ if (textView is ITextView2 textView2)
+ {
+ return textView2.TryGetTextViewLineContainingBufferPosition(bufferPosition, out textViewLine);
+ }
+ else
+ {
+ textViewLine = null;
+ return false;
+ }
}
}
}
diff --git a/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs b/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs
index b35cb6f..fc50df7 100644
--- a/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs
+++ b/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs
@@ -536,6 +536,12 @@ namespace Microsoft.VisualStudio.Text.Editor
public const string ErrorMarginWidthOptionName = "OverviewMargin/ErrorMarginWidth";
public readonly static EditorOptionKey<double> ErrorMarginWidthOptionId = new EditorOptionKey<double>(ErrorMarginWidthOptionName);
+ /// <summary>
+ /// Determines whether to have a file health indicator.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> EnableFileHealthIndicatorOptionId = new EditorOptionKey<bool>(EnableFileHealthIndicatorOptionName);
+ public const string EnableFileHealthIndicatorOptionName = "TextViewHost/FileHealthIndicator";
+
#endregion
}
@@ -658,7 +664,7 @@ namespace Microsoft.VisualStudio.Text.Editor
/// <summary>
/// Gets the default value, which is <c>WordWrapStyles.None</c>.
/// </summary>
- public override WordWrapStyles Default { get { return WordWrapStyles.None; } }
+ public override WordWrapStyles Default { get { return WordWrapStyles.AutoIndent; } }
/// <summary>
/// Gets the default text view host value.
@@ -893,7 +899,7 @@ namespace Microsoft.VisualStudio.Text.Editor
/// <summary>
/// Gets the default value, which is <c>false</c>.
/// </summary>
- public override bool Default { get { return false; } }
+ public override bool Default { get { return true; } }
/// <summary>
/// Gets the default text view host value.
@@ -1002,4 +1008,22 @@ namespace Microsoft.VisualStudio.Text.Editor
/// </summary>
public override EditorOptionKey<double> Key => DefaultTextViewOptions.CaretWidthId;
}
+
+ /// <summary>
+ /// Defines the option to enable the File Health Indicator.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultTextViewHostOptions.EnableFileHealthIndicatorOptionName)]
+ public sealed class FileHealthIndicatorEnabled : ViewOptionDefinition<bool>
+ {
+ /// <summary>
+ /// Gets the default value, which is <c>true</c>.
+ /// </summary>
+ public override bool Default { get { return true; } }
+
+ /// <summary>
+ /// Gets the default text view host value.
+ /// </summary>
+ public override EditorOptionKey<bool> Key { get { return DefaultTextViewHostOptions.EnableFileHealthIndicatorOptionId; } }
+ }
}
diff --git a/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs b/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs
index 351d733..b77878e 100644
--- a/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs
+++ b/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs
@@ -45,5 +45,13 @@ namespace Microsoft.VisualStudio.Text
/// Gets the <see cref="ITextViewLine"/> that contains the <see cref="Selection.InsertionPoint"/>.
/// </summary>
public virtual ITextViewLine ContainingTextViewLine { get; }
+
+ /// <summary>
+ /// Tries to get the <see cref="ITextViewLine"/> that contains the <see cref="Selection.InsertionPoint"/>.
+ /// This can fail if the call happens during a view layout or after the view is closed.
+ /// </summary>
+ /// <param name="line">Returns out the requested line if available, or null otherwise.</param>
+ /// <returns>True if successful, false otherwise.</returns>
+ public abstract bool TryGetContainingTextViewLine(out ITextViewLine line);
}
}
diff --git a/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs b/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs
index 05a522b..85f4f0f 100644
--- a/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs
+++ b/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Threading;
@@ -12,7 +13,7 @@ namespace Microsoft.VisualStudio.Utilities
public abstract class AbstractUIThreadOperationContext : IUIThreadOperationContext
#pragma warning restore CA1063 // Implement IDisposable Correctly
{
- private List<IUIThreadOperationScope> _scopes;
+ private ImmutableList<IUIThreadOperationScope> _scopes;
private bool _allowCancellation;
private PropertyCollection _properties;
private readonly string _defaultDescription;
@@ -30,6 +31,7 @@ namespace Microsoft.VisualStudio.Utilities
{
_defaultDescription = defaultDescription ?? throw new ArgumentNullException(nameof(defaultDescription));
_allowCancellation = allowCancellation;
+ _scopes = ImmutableList<IUIThreadOperationScope>.Empty;
}
/// <summary>
@@ -56,12 +58,14 @@ namespace Microsoft.VisualStudio.Utilities
return false;
}
- if (_scopes == null || _scopes.Count == 0)
+ ImmutableList<IUIThreadOperationScope> scopes = _scopes;
+
+ if (scopes == null || scopes.Count == 0)
{
return _allowCancellation;
}
- return _scopes.All((s) => s.AllowCancellation);
+ return scopes.All((s) => s.AllowCancellation);
}
}
@@ -78,31 +82,42 @@ namespace Microsoft.VisualStudio.Utilities
return _defaultDescription;
}
+ ImmutableList<IUIThreadOperationScope> scopes = _scopes;
+
// Most common case
- if (_scopes.Count == 1)
+ if (scopes.Count == 1)
{
- return _scopes[0].Description;
+ return scopes[0].Description;
}
// Combine descriptions of all current scopes
- return string.Join(Environment.NewLine, _scopes.Select((s) => s.Description));
+ return string.Join(Environment.NewLine, scopes.Select((s) => s.Description));
}
}
protected int CompletedItems => _completedItems;
protected int TotalItems => _totalItems;
- private IList<IUIThreadOperationScope> LazyScopes => _scopes ?? (_scopes = new List<IUIThreadOperationScope>());
-
/// <summary>
/// Gets current list of <see cref="IUIThreadOperationScope"/>s in this context.
/// </summary>
- public virtual IEnumerable<IUIThreadOperationScope> Scopes => this.LazyScopes;
+ public virtual IEnumerable<IUIThreadOperationScope> Scopes => _scopes;
/// <summary>
/// A collection of properties.
/// </summary>
- public virtual PropertyCollection Properties => _properties ?? (_properties = new PropertyCollection());
+ public virtual PropertyCollection Properties
+ {
+ get
+ {
+ if (_properties == null)
+ {
+ Interlocked.CompareExchange(ref _properties, new PropertyCollection(), null);
+ }
+
+ return _properties;
+ }
+ }
/// <summary>
/// Adds an UI thread operation scope with its own cancellability, description and progress tracker.
@@ -111,7 +126,20 @@ namespace Microsoft.VisualStudio.Utilities
public virtual IUIThreadOperationScope AddScope(bool allowCancellation, string description)
{
var scope = new UIThreadOperationScope(allowCancellation, description, this);
- this.LazyScopes.Add(scope);
+
+ while (true)
+ {
+ ImmutableList<IUIThreadOperationScope> oldScopes = _scopes;
+ ImmutableList<IUIThreadOperationScope> newScopes = oldScopes == null ? ImmutableList.Create<IUIThreadOperationScope>(scope) : oldScopes.Add(scope);
+
+ var currentScopes = Interlocked.CompareExchange(ref _scopes, newScopes, oldScopes);
+ if (currentScopes == oldScopes)
+ {
+ // No other thread preempted us, new scopes set successfully
+ break;
+ }
+ }
+
this.OnScopesChanged();
return scope;
}
@@ -120,7 +148,14 @@ namespace Microsoft.VisualStudio.Utilities
{
int completed = 0;
int total = 0;
- foreach (UIThreadOperationScope scope in this.LazyScopes)
+
+ ImmutableList<IUIThreadOperationScope> scopes = _scopes;
+ if (scopes == null)
+ {
+ return;
+ }
+
+ foreach (UIThreadOperationScope scope in scopes)
{
completed += scope.CompletedItems;
total += scope.TotalItems;
@@ -159,16 +194,37 @@ namespace Microsoft.VisualStudio.Utilities
protected virtual void OnScopeDisposed(IUIThreadOperationScope scope)
{
+ if (scope == null)
+ {
+ return;
+ }
+
_allowCancellation &= scope.AllowCancellation;
- _scopes.Remove(scope);
+
+ if (_scopes == null)
+ {
+ return;
+ }
+
+ while (true) {
+ ImmutableList<IUIThreadOperationScope> oldScopes = _scopes;
+ ImmutableList<IUIThreadOperationScope> newScopes = oldScopes.Remove(scope);
+
+ var currentScopes = Interlocked.CompareExchange(ref _scopes, newScopes, oldScopes);
+ if (currentScopes == oldScopes)
+ {
+ // No other thread preempted us, new scopes set successfully
+ break;
+ }
+ }
+
OnScopesChanged();
}
- private class UIThreadOperationScope : IUIThreadOperationScope
+ private class UIThreadOperationScope : IUIThreadOperationScope, IProgress<ProgressInfo>
{
private bool _allowCancellation;
private string _description;
- private IProgress<ProgressInfo> _progress;
private readonly AbstractUIThreadOperationContext _context;
private int _completedItems;
private int _totalItems;
@@ -208,22 +264,22 @@ namespace Microsoft.VisualStudio.Utilities
public IUIThreadOperationContext Context => _context;
- public IProgress<ProgressInfo> Progress => _progress ?? (_progress = new Progress<ProgressInfo>((progressInfo) => OnProgressChanged(progressInfo)));
+ public IProgress<ProgressInfo> Progress => this;
public int CompletedItems => _completedItems;
public int TotalItems => _totalItems;
- private void OnProgressChanged(ProgressInfo progressInfo)
+ public void Dispose()
{
- Interlocked.Exchange(ref _completedItems, progressInfo.CompletedItems);
- Interlocked.Exchange(ref _totalItems, progressInfo.TotalItems);
- _context.OnScopeProgressChanged(this);
+ _context.OnScopeDisposed(this);
}
- public void Dispose()
+ void IProgress<ProgressInfo>.Report(ProgressInfo progressInfo)
{
- _context.OnScopeDisposed(this);
+ Interlocked.Exchange(ref _completedItems, progressInfo.CompletedItems);
+ Interlocked.Exchange(ref _totalItems, progressInfo.TotalItems);
+ _context.OnScopeProgressChanged(this);
}
}
}
diff --git a/src/Text/Def/TextUI/Utilities/IStatusBarService.cs b/src/Text/Def/TextUI/Utilities/IStatusBarService.cs
new file mode 100644
index 0000000..05dc1dd
--- /dev/null
+++ b/src/Text/Def/TextUI/Utilities/IStatusBarService.cs
@@ -0,0 +1,22 @@
+using System.Threading.Tasks;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// A status bar service enabling to send messages to the editor host's status bar.
+ /// </summary>
+ /// <remarks>
+ /// <para>This is a MEF component part, and should be imported as follows:
+ /// [Import]
+ /// IStatusBarService statusBarService = null;
+ /// </para>
+ /// </remarks>
+ internal interface IStatusBarService
+ {
+ /// <summary>
+ /// Sends a text to the editor host's status bar.
+ /// </summary>
+ /// <param name="text">A text to be displayed on the status bar.</param>
+ Task SetTextAsync(string text);
+ }
+}
diff --git a/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs b/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs
index b47b0b4..37804b2 100644
--- a/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs
+++ b/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs
@@ -64,7 +64,7 @@ namespace Microsoft.VisualStudio.Utilities
/// <summary>
/// Executes the action synchronously and waits for it to complete.
/// </summary>
- /// <param name="title">Operation's title.</param>
+ /// <param name="title">Operation's title. Can be null to indicate that the wait dialog should use the application's title.</param>
/// <param name="defaultDescription">Default operation's description, which is displayed on the wait dialog unless
/// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to
/// the <see cref="IUIThreadOperationContext"/>.</param>
@@ -76,10 +76,18 @@ namespace Microsoft.VisualStudio.Utilities
Action<IUIThreadOperationContext> action);
/// <summary>
+ /// Executes the action synchronously and waits for it to complete.
+ /// </summary>
+ /// <param name="executionOptions">Options that control action execution behavior.</param>
+ /// <param name="action">An action to execute.</param>
+ /// <returns>A status of action execution.</returns>
+ UIThreadOperationStatus Execute(UIThreadOperationExecutionOptions executionOptions, Action<IUIThreadOperationContext> action);
+
+ /// <summary>
/// Begins executing potentially long running operation on the caller thread and provides a context object that provides access to shared
/// cancellability and wait indication.
/// </summary>
- /// <param name="title">Operation's title.</param>
+ /// <param name="title">Operation's title. Can be null to indicate that the wait dialog should use the application's title.</param>
/// <param name="defaultDescription">Default operation's description, which is displayed on the wait dialog unless
/// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to
/// the <see cref="IUIThreadOperationContext"/>.</param>
@@ -89,5 +97,15 @@ namespace Microsoft.VisualStudio.Utilities
/// cancellability and wait indication for the given operation. The operation is considered executed
/// when this <see cref="IUIThreadOperationContext"/> instance is disposed.</returns>
IUIThreadOperationContext BeginExecute(string title, string defaultDescription, bool allowCancellation, bool showProgress);
+
+ /// <summary>
+ /// Begins executing potentially long running operation on the caller thread and provides a context object that provides access to shared
+ /// cancellability and wait indication.
+ /// </summary>
+ /// <param name="executionOptions">Options that control execution behavior.</param>
+ /// <returns><see cref="IUIThreadOperationContext"/> instance that provides access to shared two way
+ /// cancellability and wait indication for the given operation. The operation is considered executed
+ /// when this <see cref="IUIThreadOperationContext"/> instance is disposed.</returns>
+ IUIThreadOperationContext BeginExecute(UIThreadOperationExecutionOptions executionOptions);
}
}
diff --git a/src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs b/src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs
new file mode 100644
index 0000000..2472f4c
--- /dev/null
+++ b/src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs
@@ -0,0 +1,44 @@
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// A controller that enables and controls auto-cancellation of an operation execution by
+ /// <see cref="IUIThreadOperationExecutor"/> on a timeout.
+ /// </summary>
+ public interface IUIThreadOperationTimeoutController
+ {
+ /// <summary>
+ /// The duration (in milliseconds) after which an operation shouold be auto-cancelled.
+ /// </summary>
+ /// <remarks><see cref="Timeout.Infinite"/> disables auto-cancellation.</remarks>
+ int CancelAfter { get; }
+
+ /// <summary>
+ /// Gets whether an operation, whose execution time exceeded <see cref="CancelAfter"/> timeout should be
+ /// cancelled.
+ /// </summary>
+ /// <remarks>This callback can be used to disable auto-cancellation when an operation already
+ /// passed the point of no cancellation and it would leave system in an inconsistent state.
+ /// This method is called on a background thread.</remarks>
+ bool ShouldCancel();
+
+ /// <summary>
+ /// An event callback raised when an operation execution timeout was reached.
+ /// </summary>
+ /// <param name="wasExecutionCancelled">Indicates whether an operation was auto-cancelled.
+ /// Might be <c>false</c> if the operation is not cancellable (<see cref="IUIThreadOperationContext.AllowCancellation"/>
+ /// is <c>false</c> or <see cref="ShouldCancel"/> returned <c>false</c>.
+ /// </param>7
+ /// <remarks>This method is called on a background thread.</remarks>
+ void OnTimeout(bool wasExecutionCancelled);
+
+ /// <summary>
+ /// An event callback raised when a UI thread operation execution took long enough to be considered
+ /// as a delay. Visual Studio implementation of the <see cref="IUIThreadOperationExecutor"/> displays
+ /// a wait dialog at this point.
+ /// </summary>
+ /// <remarks>This method is called on a background thread.</remarks>
+ void OnDelay();
+ }
+}
diff --git a/src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs b/src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs
new file mode 100644
index 0000000..7a5cf8a
--- /dev/null
+++ b/src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// Options that control behavior of <see cref="IUIThreadOperationExecutor"/>.
+ /// </summary>
+ public class UIThreadOperationExecutionOptions
+ {
+ /// <summary>
+ /// Operation's title.
+ /// </summary>
+ public string Title { get; }
+
+ /// <summary>
+ /// Default operation's description, which is displayed on the wait dialog unless
+ /// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to
+ /// the <see cref="IUIThreadOperationContext"/>.
+ /// </summary>
+ public string DefaultDescription { get; }
+
+ /// <summary>
+ /// Whether to allow cancellability.
+ /// </summary>
+ public bool AllowCancellation { get; }
+
+ /// <summary>
+ /// Whether to show progress indication.
+ /// </summary>
+ public bool ShowProgress { get; }
+
+ /// <summary>
+ /// A controller that enables and controls auto-cancellation of an operation execution on a timeout.
+ /// </summary>
+ public IUIThreadOperationTimeoutController TimeoutController { get; }
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="UIThreadOperationExecutionOptions"/>.
+ /// </summary>
+ /// <param name="title">Operation's title. Can be null to indicate that the wait dialog should use the application's title.</param>
+ /// <param name="defaultDescription">Default operation's description, which is displayed on the wait dialog unless
+ /// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to
+ /// the <see cref="IUIThreadOperationContext"/>.</param>
+ /// <param name="allowCancellation">Whether to allow cancellability.</param>
+ /// <param name="showProgress">Whether to show progress indication.</param>
+ /// <param name="timeoutController">A controller that enables and controls auto-cancellation of an operation execution on a timeout.</param>
+ public UIThreadOperationExecutionOptions(string title, string defaultDescription, bool allowCancellation, bool showProgress, IUIThreadOperationTimeoutController timeoutController = null)
+ {
+ Title = title;
+ DefaultDescription = defaultDescription ?? throw new ArgumentNullException(nameof(defaultDescription));
+ AllowCancellation = allowCancellation;
+ ShowProgress = showProgress;
+
+ // Timeout.Infinite is -1, other than that any negative value is invalid
+ if (timeoutController?.CancelAfter < Timeout.Infinite)
+ {
+ throw new ArgumentOutOfRangeException(nameof(timeoutController));
+ }
+
+ TimeoutController = timeoutController;
+ }
+ }
+}
diff --git a/src/Text/Impl/Commanding/CommandingStrings.Designer.cs b/src/Text/Impl/Commanding/CommandingStrings.Designer.cs
index 714059e..8d2e4db 100644
--- a/src/Text/Impl/Commanding/CommandingStrings.Designer.cs
+++ b/src/Text/Impl/Commanding/CommandingStrings.Designer.cs
@@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class CommandingStrings {
@@ -61,6 +61,15 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation {
}
/// <summary>
+ /// Looks up a localized string similar to Command handler &apos;{0}&apos; has exceeded allotted timeout and was auto canceled..
+ /// </summary>
+ internal static string CommandCancelled {
+ get {
+ return ResourceManager.GetString("CommandCancelled", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Please wait for an editor command to finish....
/// </summary>
internal static string WaitForCommandExecution {
diff --git a/src/Text/Impl/Commanding/CommandingStrings.resx b/src/Text/Impl/Commanding/CommandingStrings.resx
index b05561d..9286e4d 100644
--- a/src/Text/Impl/Commanding/CommandingStrings.resx
+++ b/src/Text/Impl/Commanding/CommandingStrings.resx
@@ -101,4 +101,7 @@
<data name="WaitForCommandExecution" xml:space="preserve">
<value>Please wait for an editor command to finish...</value>
</data>
+ <data name="CommandCancelled" xml:space="preserve">
+ <value>Command handler '{0}' has exceeded allotted timeout and was auto canceled.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/Text/Impl/Commanding/EditorCommandHandlerService.cs b/src/Text/Impl/Commanding/EditorCommandHandlerService.cs
index 4ced5c3..c9730e7 100644
--- a/src/Text/Impl/Commanding/EditorCommandHandlerService.cs
+++ b/src/Text/Impl/Commanding/EditorCommandHandlerService.cs
@@ -1,27 +1,30 @@
using System;
-using System.Linq;
using System.Collections.Generic;
-using Microsoft.VisualStudio.Utilities;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
using Microsoft.VisualStudio.Commanding;
-using Microsoft.VisualStudio.Text.Utilities;
-using Microsoft.VisualStudio.Text.Editor.Commanding;
-using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text;
-using Microsoft.VisualStudio.Threading;
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Editor.Commanding;
+using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
+using Microsoft.VisualStudio.Text.Utilities;
+using Microsoft.VisualStudio.Utilities;
using ICommandHandlerAndMetadata = System.Lazy<Microsoft.VisualStudio.Commanding.ICommandHandler, Microsoft.VisualStudio.UI.Text.Commanding.Implementation.ICommandHandlerMetadata>;
-using System.Runtime.CompilerServices;
namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
internal class EditorCommandHandlerService : IEditorCommandHandlerService
{
+ private const string TelemetryEventPrefix = "VS/Editor/Commanding";
+ private const string TelemetryPropertyPrefix = "VS.Editor.Commanding";
+
private readonly IEnumerable<ICommandHandlerAndMetadata> _commandHandlers;
- private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
- private readonly JoinableTaskContext _joinableTaskContext;
+ private readonly EditorCommandHandlerServiceFactory _factory;
private readonly ITextView _textView;
- private readonly IComparer<IEnumerable<string>> _contentTypesComparer;
private readonly ICommandingTextBufferResolver _bufferResolver;
- private readonly IGuardedOperations _guardedOperations;
private readonly static IReadOnlyList<ICommandHandlerAndMetadata> EmptyHandlerList = new List<ICommandHandlerAndMetadata>(0);
private readonly static Action EmptyAction = delegate { };
@@ -32,26 +35,21 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
/// handlers every time we need handlers of a specific type, for a given content type.
private readonly Dictionary<(Type commandArgType, IContentType contentType), IReadOnlyList<ICommandHandlerAndMetadata>> _commandHandlersByTypeAndContentType;
- public EditorCommandHandlerService(ITextView textView,
+ public EditorCommandHandlerService(EditorCommandHandlerServiceFactory factory,
+ ITextView textView,
IEnumerable<ICommandHandlerAndMetadata> commandHandlers,
- IUIThreadOperationExecutor uiThreadOperationExecutor, JoinableTaskContext joinableTaskContext,
- IComparer<IEnumerable<string>> contentTypesComparer,
- ICommandingTextBufferResolver bufferResolver,
- IGuardedOperations guardedOperations)
+ ICommandingTextBufferResolver bufferResolver)
{
_commandHandlers = commandHandlers ?? throw new ArgumentNullException(nameof(commandHandlers));
+ _factory = factory ?? throw new ArgumentNullException(nameof(factory));
_textView = textView ?? throw new ArgumentNullException(nameof(textView));
- _uiThreadOperationExecutor = uiThreadOperationExecutor ?? throw new ArgumentNullException(nameof(uiThreadOperationExecutor));
- _joinableTaskContext = joinableTaskContext ?? throw new ArgumentNullException(nameof(joinableTaskContext));
- _contentTypesComparer = contentTypesComparer ?? throw new ArgumentNullException(nameof(contentTypesComparer));
_commandHandlersByTypeAndContentType = new Dictionary<(Type commandArgType, IContentType contentType), IReadOnlyList<ICommandHandlerAndMetadata>>();
_bufferResolver = bufferResolver ?? throw new ArgumentNullException(nameof(bufferResolver));
- _guardedOperations = guardedOperations ?? throw new ArgumentNullException(nameof(guardedOperations));
}
public CommandState GetCommandState<T>(Func<ITextView, ITextBuffer, T> argsFactory, Func<CommandState> nextCommandHandler) where T : EditorCommandArgs
{
- if (!_joinableTaskContext.IsOnMainThread)
+ if (!_factory.JoinableTaskContext.IsOnMainThread)
{
throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.GetCommandState)} method shoudl only be called on the UI thread.");
}
@@ -91,7 +89,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
public void Execute<T>(Func<ITextView, ITextBuffer, T> argsFactory, Action nextCommandHandler) where T : EditorCommandArgs
{
- if (!_joinableTaskContext.IsOnMainThread)
+ if (!_factory.JoinableTaskContext.IsOnMainThread)
{
throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.Execute)} method shoudl only be called on the UI thread.");
}
@@ -105,10 +103,10 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
return;
}
+ EditorCommandHandlerServiceState state = null;
+
using (var reentrancyGuard = new ReentrancyGuard(_textView))
{
- CommandExecutionContext commandExecutionContext = null;
-
// Build up chain of handlers per buffer
Action handlerChain = nextCommandHandler ?? EmptyAction;
// TODO: realize the chain dynamically and without Reverse()
@@ -123,6 +121,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
// Args factory failed, skip command handlers and just call next
handlerChain();
+ return;
}
if (handler is IDynamicCommandHandler<T> dynamicCommandHandler &&
@@ -132,29 +131,55 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
continue;
}
- if (commandExecutionContext == null)
+ if (state == null)
{
- commandExecutionContext = CreateCommandExecutionContext();
+ state = InitializeExecutionState(args);
}
- handlerChain = () => _guardedOperations.CallExtensionPoint(handler,
- () => handler.ExecuteCommand(args, nextHandler, commandExecutionContext),
+ handlerChain = () => _factory.GuardedOperations.CallExtensionPoint(handler,
+ () =>
+ {
+ state.OnExecutingCommandHandlerBegin(handler);
+ handler.ExecuteCommand(args, nextHandler, state.ExecutionContext);
+ state.OnExecutingCommandHandlerEnd(handler);
+ },
// Do not guard against cancellation exceptions, they are handled by ExecuteCommandHandlerChain
- exceptionGuardFilter: (e) => !IsOperationCancelledException(e));
+ exceptionGuardFilter: (e) => !IsOperationCancelledException(e));
}
- ExecuteCommandHandlerChain(commandExecutionContext, handlerChain, nextCommandHandler);
+ if (state == null)
+ {
+ // No matching command handlers, just call next
+ handlerChain();
+ return;
+ }
+
+ ExecuteCommandHandlerChain(state, handlerChain, nextCommandHandler);
}
}
+ private EditorCommandHandlerServiceState InitializeExecutionState<T>(T args) where T : EditorCommandArgs
+ {
+ var state = new EditorCommandHandlerServiceState(args, IsTypingCommand(args));
+ var uiThreadOperationContext = _factory.UIThreadOperationExecutor.BeginExecute(
+ new UIThreadOperationExecutionOptions(
+ title: null, // We want same caption as the main window
+ defaultDescription: WaitForCommandExecutionString, allowCancellation: true, showProgress: true,
+ timeoutController: new TimeoutController(state, _textView, _factory.LoggingService)));
+ var commandExecutionContext = new CommandExecutionContext(uiThreadOperationContext);
+ commandExecutionContext.OperationContext.UserCancellationToken.Register(OnExecutionCancellationRequested, state);
+ state.ExecutionContext = commandExecutionContext;
+ return state;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsOperationCancelledException(Exception e)
{
return e is OperationCanceledException || e is AggregateException aggregate && aggregate.InnerExceptions.All(ie => ie is OperationCanceledException);
}
- private static void ExecuteCommandHandlerChain(
- CommandExecutionContext commandExecutionContext,
+ private void ExecuteCommandHandlerChain(
+ EditorCommandHandlerServiceState state,
Action handlerChain,
Action nextCommandHandler)
{
@@ -162,21 +187,47 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
// Kick off the first command handler.
handlerChain();
+ if (state.ExecutionContext.OperationContext.UserCancellationToken.IsCancellationRequested)
+ {
+ LogCancellationWasIgnored(state);
+ }
}
catch (OperationCanceledException)
{
- nextCommandHandler?.Invoke();
+ OnCommandExecutionCancelled(nextCommandHandler, state);
}
catch (AggregateException aggregate) when (aggregate.InnerExceptions.All(e => e is OperationCanceledException))
{
- nextCommandHandler?.Invoke();
+ OnCommandExecutionCancelled(nextCommandHandler, state);
}
finally
{
- commandExecutionContext?.OperationContext?.Dispose();
+ state.ExecutionContext?.OperationContext?.Dispose();
}
}
+ private void OnExecutionCancellationRequested(object state)
+ {
+ Debug.Assert(!_factory.JoinableTaskContext.IsOnMainThread);
+ ((EditorCommandHandlerServiceState)state).OnExecutionCancellationRequested();
+ }
+
+ private void OnCommandExecutionCancelled(Action nextCommandHandler, EditorCommandHandlerServiceState state)
+ {
+ var executingHandler = state.GetCurrentlyExecutingCommandHander();
+ var executingCommand = state.ExecutingCommand;
+ bool userCancelled = !state.ExecutionHasTimedOut;
+ _factory.JoinableTaskContext.Factory.RunAsync(async () =>
+ {
+ LogCommandExecutionCancelled(executingHandler, executingCommand, userCancelled);
+
+ string statusBarMessage = string.Format(CultureInfo.CurrentCulture, CommandingStrings.CommandCancelled, executingHandler?.DisplayName);
+ await _factory.StatusBar.SetTextAsync(statusBarMessage).ConfigureAwait(false);
+ });
+
+ nextCommandHandler?.Invoke();
+ }
+
private class ReentrancyGuard : IDisposable
{
private readonly IPropertyOwner _owner;
@@ -198,15 +249,6 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
return _textView.Properties.ContainsProperty(typeof(ReentrancyGuard));
}
- private CommandExecutionContext CreateCommandExecutionContext()
- {
- CommandExecutionContext commandExecutionContext;
- var uiThreadOperationContext = _uiThreadOperationExecutor.BeginExecute(title: null, // We want same caption as the main window
- defaultDescription: WaitForCommandExecutionString, allowCancellation: true, showProgress: true);
- commandExecutionContext = new CommandExecutionContext(uiThreadOperationContext);
- return commandExecutionContext;
- }
-
//internal for unit tests
internal IEnumerable<(ITextBuffer buffer, ICommandHandler handler)> GetOrderedBuffersAndCommandHandlers<T>() where T : EditorCommandArgs
{
@@ -289,7 +331,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
var handler = handlerBuckets[i].Peek();
// Can this handler handle content type more specific than top handler in firstNonEmptyBucket?
- if (_contentTypesComparer.Compare(handler.Metadata.ContentTypes, currentHandler.Metadata.ContentTypes) < 0)
+ if (_factory.ContentTypeComparer.Compare(handler.Metadata.ContentTypes, currentHandler.Metadata.ContentTypes) < 0)
{
foundBetterHandler = true;
handlerBuckets[i].Pop();
@@ -316,7 +358,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
IList<ICommandHandlerAndMetadata> newCommandHandlerList = null;
foreach (var lazyCommandHandler in SelectMatchingCommandHandlers(_commandHandlers, contentType, textViewRoles))
{
- var commandHandler = _guardedOperations.InstantiateExtension<ICommandHandler>(this, lazyCommandHandler);
+ var commandHandler = _factory.GuardedOperations.InstantiateExtension<ICommandHandler>(this, lazyCommandHandler);
if (commandHandler is ICommandHandler<T> || commandHandler is IChainedCommandHandler<T>)
{
if (newCommandHandlerList == null)
@@ -389,5 +431,82 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
return false;
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsTypingCommand(EditorCommandArgs args)
+ {
+ // TODO: temporarily only include typechar to not break Roslyn inline rename and other non-typing scenario, tracked by #657668
+ return args is TypeCharCommandArgs;
+ //args is DeleteKeyCommandArgs ||
+ //args is ReturnKeyCommandArgs ||
+ //args is BackspaceKeyCommandArgs ||
+ //args is TabKeyCommandArgs ||
+ //args is UndoCommandArgs ||
+ //args is RedoCommandArgs;
+ }
+
+ private void LogCommandExecutionCancelled(INamed executingHandler, EditorCommandArgs executingCommand, bool userCancelled)
+ {
+ _factory.LoggingService?.PostEvent($"{TelemetryEventPrefix}/ExecutionCancelled",
+ $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.CommandHandler", executingHandler?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.UserCancelled", userCancelled);
+ }
+
+ private void LogCancellationWasIgnored(EditorCommandHandlerServiceState state)
+ {
+ bool userCancelled = !state.ExecutionHasTimedOut;
+ var executingCommand = state.ExecutingCommand;
+ _factory.LoggingService?.PostEvent($"{TelemetryEventPrefix}/IgnoredExecutionCancellation",
+ $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.CommandHandler", state.CommandHandlerExecutingDuringCancellationRequest?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.UserCancelled", userCancelled);
+ }
+
+ private class TimeoutController : IUIThreadOperationTimeoutController
+ {
+ private readonly EditorCommandHandlerServiceState _state;
+ private readonly ITextView _textView;
+ private readonly ILoggingServiceInternal _loggingService;
+
+ public TimeoutController(EditorCommandHandlerServiceState state, ITextView textView, ILoggingServiceInternal loggingService)
+ {
+ _state = state;
+ _textView = textView;
+ _loggingService = loggingService;
+ }
+
+ public int CancelAfter
+ => _state.IsExecutingTypingCommand ?
+ _textView.Options.GetOptionValue(DefaultOptions.MaximumTypingLatencyOptionId) :
+ Timeout.Infinite;
+
+ public bool ShouldCancel()
+ {
+ // TODO: this needs to allow non typing command scenarios for example hitting return in inline rename, tracked by #657668
+ return _state.IsExecutingTypingCommand;
+ }
+
+ public void OnTimeout(bool wasExecutionCancelled)
+ {
+ Debug.Assert(_state.IsExecutingTypingCommand);
+ _state.ExecutionHasTimedOut = true;
+ var executingCommand = _state.ExecutingCommand;
+
+ _loggingService?.PostEvent($"{TelemetryEventPrefix}/ExecutionTimeout",
+ $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.CommandHandler", _state.GetCurrentlyExecutingCommandHander()?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.Timeout", this.CancelAfter,
+ $"{TelemetryPropertyPrefix}.WasExecutionCancelled", wasExecutionCancelled);
+ }
+
+ public void OnDelay()
+ {
+ var executingCommand = _state.ExecutingCommand;
+ _loggingService?.PostEvent($"{TelemetryEventPrefix}/WaitDialogShown",
+ $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.CommandHandler", _state.GetCurrentlyExecutingCommandHander()?.GetType().FullName);
+ }
+ }
}
}
diff --git a/src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs b/src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs
index f38a85c..a2eac95 100644
--- a/src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs
+++ b/src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs
@@ -8,6 +8,7 @@ using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Threading;
using System.Linq;
using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Utilities;
namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
@@ -16,11 +17,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
private readonly IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> _commandHandlers;
private readonly IList<Lazy<ICommandingTextBufferResolverProvider, IContentTypeMetadata>> _bufferResolverProviders;
- private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
- private readonly JoinableTaskContext _joinableTaskContext;
private readonly IContentTypeRegistryService _contentTypeRegistryService;
- private readonly IGuardedOperations _guardedOperations;
- private readonly StableContentTypeComparer _contentTypeComparer;
[ImportingConstructor]
public EditorCommandHandlerServiceFactory(
@@ -28,14 +25,19 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
[ImportMany]IEnumerable<Lazy<ICommandingTextBufferResolverProvider, IContentTypeMetadata>> bufferResolvers,
IUIThreadOperationExecutor uiThreadOperationExecutor,
JoinableTaskContext joinableTaskContext,
+ IStatusBarService statusBar,
IContentTypeRegistryService contentTypeRegistryService,
- IGuardedOperations guardedOperations)
+ IGuardedOperations guardedOperations,
+ [Import(AllowDefault = true)] ILoggingServiceInternal loggingService)
{
- _uiThreadOperationExecutor = uiThreadOperationExecutor;
- _joinableTaskContext = joinableTaskContext;
- _guardedOperations = guardedOperations;
+ UIThreadOperationExecutor = uiThreadOperationExecutor;
+ JoinableTaskContext = joinableTaskContext;
+ StatusBar = statusBar;
+ GuardedOperations = guardedOperations;
+ LoggingService = loggingService;
+
_contentTypeRegistryService = contentTypeRegistryService;
- _contentTypeComparer = new StableContentTypeComparer(_contentTypeRegistryService);
+ ContentTypeComparer = new StableContentTypeComparer(_contentTypeRegistryService);
_commandHandlers = OrderCommandHandlers(commandHandlers);
if (!bufferResolvers.Any())
{
@@ -45,16 +47,27 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
_bufferResolverProviders = bufferResolvers.ToList();
}
+ internal IGuardedOperations GuardedOperations { get; }
+
+ internal ILoggingServiceInternal LoggingService { get; }
+
+ internal JoinableTaskContext JoinableTaskContext { get; }
+
+ internal IUIThreadOperationExecutor UIThreadOperationExecutor { get; }
+
+ internal IStatusBarService StatusBar { get; }
+
+ internal StableContentTypeComparer ContentTypeComparer { get; }
+
public IEditorCommandHandlerService GetService(ITextView textView)
{
return textView.Properties.GetOrCreateSingletonProperty(() =>
{
- var bufferResolverProvider = _guardedOperations.InvokeBestMatchingFactory(_bufferResolverProviders, textView.TextBuffer.ContentType, _contentTypeRegistryService, errorSource: this);
+ var bufferResolverProvider = GuardedOperations.InvokeBestMatchingFactory(_bufferResolverProviders, textView.TextBuffer.ContentType, _contentTypeRegistryService, errorSource: this);
ICommandingTextBufferResolver bufferResolver = null;
- _guardedOperations.CallExtensionPoint(() => bufferResolver = bufferResolverProvider.CreateResolver(textView));
+ GuardedOperations.CallExtensionPoint(() => bufferResolver = bufferResolverProvider.CreateResolver(textView));
bufferResolver = bufferResolver ?? new DefaultBufferResolver(textView);
- return new EditorCommandHandlerService(textView, _commandHandlers, _uiThreadOperationExecutor, _joinableTaskContext,
- _contentTypeComparer, bufferResolver, _guardedOperations);
+ return new EditorCommandHandlerService(this, textView, _commandHandlers, bufferResolver);
});
}
@@ -69,14 +82,12 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
// buffer can be used by another text view, see https://devdiv.visualstudio.com/DevDiv/_workitems/edit/563472.
// There is no good way to cache it without holding onto the buffer (which can be disconnected
// from the text view anytime).
- return new EditorCommandHandlerService(textView, _commandHandlers, _uiThreadOperationExecutor,
- _joinableTaskContext, _contentTypeComparer,
- new SingleBufferResolver(subjectBuffer), _guardedOperations);
+ return new EditorCommandHandlerService(this, textView, _commandHandlers, new SingleBufferResolver(subjectBuffer));
}
private IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> OrderCommandHandlers(IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> commandHandlers)
{
- return commandHandlers.OrderBy((handler) => handler.Metadata.ContentTypes, _contentTypeComparer);
+ return commandHandlers.OrderBy((handler) => handler.Metadata.ContentTypes, ContentTypeComparer);
}
}
}
diff --git a/src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs b/src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs
new file mode 100644
index 0000000..d94ec07
--- /dev/null
+++ b/src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using Microsoft.VisualStudio.Commanding;
+using Microsoft.VisualStudio.Text.Editor.Commanding;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
+{
+ internal class EditorCommandHandlerServiceState
+ {
+ private readonly ConcurrentStack<ICommandHandler> _executingCommandHandlers = new ConcurrentStack<ICommandHandler>();
+
+ public CommandExecutionContext ExecutionContext { get; set; }
+ public INamed CommandHandlerExecutingDuringCancellationRequest { get; set; }
+ public bool ExecutionHasTimedOut { get; set; }
+ public EditorCommandArgs ExecutingCommand { get; }
+ public bool IsExecutingTypingCommand { get; }
+
+ public EditorCommandHandlerServiceState(EditorCommandArgs executingCommand, bool isTypingCommand)
+ {
+ _executingCommandHandlers = new ConcurrentStack<ICommandHandler>();
+ ExecutingCommand = executingCommand ?? throw new ArgumentNullException(nameof(executingCommand));
+ IsExecutingTypingCommand = isTypingCommand;
+ }
+
+ public INamed GetCurrentlyExecutingCommandHander()
+ {
+ if (_executingCommandHandlers.TryPeek(out ICommandHandler handler) &&
+ handler is INamed namedHandler)
+ {
+ return namedHandler;
+ }
+
+ return null;
+ }
+
+ public void OnExecutingCommandHandlerBegin(ICommandHandler handler)
+ {
+ _executingCommandHandlers.Push(handler);
+ }
+
+ public void OnExecutingCommandHandlerEnd(ICommandHandler handler)
+ {
+ bool success = _executingCommandHandlers.TryPop(out var topCommandHandler);
+ Debug.Assert(success, "Unexpectedly empty command handler execution stack.");
+ Debug.Assert(handler == topCommandHandler, "Unexpected command hanlder on top of the stack.");
+ }
+
+ public void OnExecutionCancellationRequested()
+ {
+ CommandHandlerExecutingDuringCancellationRequest = GetCurrentlyExecutingCommandHander();
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/EditorOperations.cs b/src/Text/Impl/EditorOperations/EditorOperations.cs
index 369273b..4672260 100644
--- a/src/Text/Impl/EditorOperations/EditorOperations.cs
+++ b/src/Text/Impl/EditorOperations/EditorOperations.cs
@@ -55,7 +55,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
ClearVirtualSpace
};
-#region Private Members
+ #region Private Members
ITextView _textView;
EditorOperationsFactoryService _factory;
@@ -80,7 +80,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
/// </summary>
private const string _boxSelectionCutCopyTag = "MSDEVColumnSelect";
-#endregion // Private Members
+ #endregion // Private Members
/// <summary>
/// Constructs an <see cref="EditorOperations"/> bound to a given <see cref="ITextView"/>.
@@ -121,7 +121,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
}
-#region IEditorOperations2 Members
+ #region IEditorOperations2 Members
public bool MoveSelectedLinesUp()
{
@@ -153,7 +153,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
endViewLine = _textView.GetTextViewLineContainingBufferPosition(_textView.Selection.End.Position - 1);
}
-#region Initial Asserts
+ #region Initial Asserts
Debug.Assert(_textView.Selection.Start.Position.Snapshot == _textView.TextSnapshot, "Selection is out of sync with view.");
@@ -161,7 +161,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
Debug.Assert(_textView.TextSnapshot == snapshot, "Text view lines are out of sync with the view");
-#endregion
+ #endregion
// check if we are at the top of the file, or trying to move a blank line
if (startLine.LineNumber < 1 || start == end)
@@ -363,7 +363,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
endViewLine = _textView.GetTextViewLineContainingBufferPosition(_textView.Selection.End.Position - 1);
}
-#region Initial Asserts
+ #region Initial Asserts
Debug.Assert(_textView.Selection.Start.Position.Snapshot == _textView.TextSnapshot, "Selection is out of sync with view.");
@@ -371,7 +371,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
Debug.Assert(_textView.TextSnapshot == snapshot, "Text view lines are out of sync with the view");
-#endregion
+ #endregion
// check if we are at the end of the file
if ((endLine.LineNumber + 1) >= snapshot.LineCount)
@@ -568,10 +568,10 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return line;
}
-#endregion
+ #endregion
-#region IEditorOperations Members
+ #region IEditorOperations Members
public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint)
{
@@ -1613,45 +1613,261 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
/// </summary>
public bool Indent()
{
- bool insertTabs = _textView.Selection.Mode == TextSelectionMode.Box || !IndentOperationShouldBeMultiLine;
+ if (!IndentWillCreateEdit())
+ {
+ // Indent box selection in virtual whitespace, if any.
+ if (_multiSelectionBroker.IsBoxSelection)
+ {
+ int indentSize = _editorOptions.GetIndentSize();
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxIndentForSelectionPoint(_multiSelectionBroker.BoxSelection.AnchorPoint, indentSize);
+ VirtualSnapshotPoint? activePoint = CalculateBoxIndentForSelectionPoint(_multiSelectionBroker.BoxSelection.ActivePoint, indentSize);
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+ }
+
+ return true;
+ }
Func<bool> action = () =>
{
- if (insertTabs)
+ using (_multiSelectionBroker.BeginBatchOperation())
{
- return this.EditHelper(edit =>
+ if (_multiSelectionBroker.IsBoxSelection)
{
- int tabSize = _editorOptions.GetTabSize();
- int indentSize = _editorOptions.GetIndentSize();
- bool convertTabsToSpaces = _editorOptions.IsConvertTabsToSpacesEnabled();
- bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box &&
- _textView.Selection.Start != _textView.Selection.End;
+ if (!TryIndentBoxSelection())
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!TryIndentAndFixupStreamSelections())
+ {
+ return false;
+ }
+ }
- // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
- // through a text change.
- VirtualSnapshotPoint? anchorPoint = (boxSelection) ? CalculateBoxIndentForSelectionPoint(_textView.Selection.AnchorPoint, indentSize) : null;
- VirtualSnapshotPoint? activePoint = (boxSelection) ? CalculateBoxIndentForSelectionPoint(_textView.Selection.ActivePoint, indentSize) : null;
+ _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll);
+
+ return true;
+ }
+ };
+
+ return ExecuteAction(Strings.InsertTab, action, SelectionUpdate.Ignore, ensureVisible: false);
+ }
- // Insert an indent for each portion of the selection (with an empty selection, there will only be a single
- // span).
- foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ private bool IndentWillCreateEdit()
+ {
+ var allSelections = _multiSelectionBroker.AllSelections;
+ if (_multiSelectionBroker.IsBoxSelection)
+ {
+ // Box selection can only create an edit if at least one non-virtual subspan is of non-zero length.
+ for (int i = 0; i < allSelections.Count; i++)
+ {
+ var selection = allSelections[i];
+ if (!selection.Start.IsInVirtualSpace)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ for (int i = 0; i < allSelections.Count; i++)
+ {
+ var selection = allSelections[i];
+
+ // Carets (zero-width selections) always produce an edit.
+ if (selection.IsEmpty)
+ {
+ return true;
+ }
+ else
+ {
+ var startLine = selection.Start.Position.GetContainingLine();
+ var endLine = selection.End.Position.GetContainingLine();
+
+ // Selections that start and end on the same line create an edit if and only if the line is non-zero in length.
+ if (startLine.LineNumber == endLine.LineNumber)
+ {
+ return (startLine.Length > 0);
+ }
+ else
+ {
+ // Only edits for multiline selections if one of the contained lines is
+ // non-zero in length because empty lines have nothing to indent.
+ for (int j = startLine.LineNumber; j < endLine.LineNumber; j++)
{
- if (!InsertIndentForSpan(span, edit, exactlyOneIndentLevel: false))
- return false;
+ if (startLine.Snapshot.GetLineFromLineNumber(j).Length > 0)
+ {
+ return true;
+ }
}
+ }
+ }
+ }
- FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+ return false;
+ }
- return true;
- });
+ private bool TryIndentAndFixupStreamSelections()
+ {
+ var newSelections = new FrugalList<(Selection old, Selection newSel)>();
+ if (!EditHelper((edit) => IndentStreamSelections(edit, newSelections)))
+ {
+ return false;
+ }
+
+ _multiSelectionBroker.PerformActionOnAllSelections(transformer =>
+ {
+ var selection = transformer.Selection;
+ if (selection.IsEmpty && selection.Extent.IsInVirtualSpace)
+ {
+ // Remove all virtual space from selection.
+ transformer.MoveTo(
+ new VirtualSnapshotPoint(selection.AnchorPoint.Position),
+ new VirtualSnapshotPoint(selection.ActivePoint.Position),
+ new VirtualSnapshotPoint(selection.InsertionPoint.Position),
+ PositionAffinity.Successor);
+ }
+ });
+
+ // Apply selection changes after the edit so we can apply the desired point tracking mode.
+ // Preserves pre-multicaret behavior by keeping the left edge of selections unmodified
+ // when the user presses 'tab' using negative tracking.
+ TransformSelections(newSelections);
+
+ return true;
+ }
+
+ private void TransformSelections(FrugalList<(Selection old, Selection newSel)> newSelections)
+ {
+ foreach (var (oldSelection, newSelection) in newSelections)
+ {
+ var start = newSelection.Start.TranslateTo(_multiSelectionBroker.CurrentSnapshot, PointTrackingMode.Negative);
+ var end = newSelection.End.TranslateTo(_multiSelectionBroker.CurrentSnapshot, PointTrackingMode.Positive);
+ var insertion = newSelection.InsertionPoint.TranslateTo(
+ _multiSelectionBroker.CurrentSnapshot,
+ newSelection.IsReversed ? PointTrackingMode.Negative : PointTrackingMode.Positive);
+
+ _multiSelectionBroker.TryPerformActionOnSelection(
+ oldSelection,
+ transformer => transformer.MoveTo(
+ newSelection.IsReversed ? end : start,
+ newSelection.IsReversed ? start : end,
+ insertion,
+ newSelection.InsertionPointAffinity),
+ out _);
+ }
+ }
+
+ private bool IndentStreamSelections(ITextEdit edit, IList<(Selection old, Selection newSel)> newSelections)
+ {
+ // Tracks whether or not an edit succeeded. Edits can fail if the caret is in a readonly region.
+ bool succeeded = true;
+
+ // Compound edits perform multiple actions with reference to the original snapshot. This tracks
+ // the column adjustment of the previous caret so that we can align the subsequent caret with the
+ // correct tabstop when there are multiple carets on the same line.
+ var columnOffset = default(int?);
+
+ // There is a particular edge case where there can be multiple multi-line selections on the same line.
+ // Naively indenting one tabstop per selection would cause this line to be indented twice so we must
+ // keep track of which line the previous multi-line selection ended on.
+ int? previousSelectionEndLineNumber = 0;
+ bool previousSelectionWasMultiline = false;
+
+ _multiSelectionBroker.PerformActionOnAllSelections((transformer) =>
+ {
+ int? newSelectionStartLineNumber = transformer.Selection.Start.Position.GetContainingLine().LineNumber;
+ int? newSelectionEndLineNumber = transformer.Selection.End.Position.GetContainingLine().LineNumber;
+
+ // Reset the column offset if we've transitioned to the next line.
+ if (newSelectionStartLineNumber != previousSelectionEndLineNumber)
+ {
+ columnOffset = null;
+ }
+
+ previousSelectionWasMultiline = IndentOperationShouldBeMultiLine(transformer.Selection);
+
+ if (IndentOperationShouldBeMultiLine(transformer.Selection))
+ {
+ // Don't indent this line if we are the second multi-line selection intersecting this line
+ // or else we'll cause a double-indent.
+ bool skipFirstLine = (columnOffset != null) && previousSelectionEndLineNumber == newSelectionStartLineNumber && previousSelectionWasMultiline;
+
+ // In case there are multiple carets on a single line, we have to remember the column
+ // to ensure that the subsequent carets on the same line receive adequate indentation.
+ columnOffset = PerformIndentActionOnEachBufferLine(
+ edit,
+ newSelections,
+ skipFirstLine ? MultiLineSelectionWithoutFirstLine(transformer.Selection) : transformer.Selection,
+ InsertSingleIndentAtPoint);
+ if (columnOffset == null)
+ {
+ succeeded = false;
+ return;
+ }
}
else
{
- return PerformIndentActionOnEachBufferLine(InsertSingleIndentAtPoint);
+ // In case there are multiple carets on a single line, we have to remember the column
+ // to ensure that the subsequent carets on the same line receive adequate indentation.
+ columnOffset = InsertIndentForSpan(transformer.Selection.Extent, edit, exactlyOneIndentLevel: false, false, columnOffset: columnOffset ?? 0);
+ if (columnOffset == null)
+ {
+ succeeded = false;
+ return;
+ }
}
- };
- return ExecuteAction(Strings.InsertTab, action, (_textView.Selection.IsEmpty) ? SelectionUpdate.ClearVirtualSpace : SelectionUpdate.Ignore, ensureVisible: insertTabs);
+ previousSelectionEndLineNumber = newSelectionEndLineNumber;
+ });
+
+ return succeeded;
+ }
+
+ private static Selection MultiLineSelectionWithoutFirstLine(Selection selection)
+ {
+ var snapshot = selection.Start.Position.Snapshot;
+ var firstLineNumber = selection.Start.Position.GetContainingLine().LineNumber;
+
+ // Requires the selection to be multiple lines.
+ Debug.Assert(firstLineNumber <= snapshot.LineCount);
+
+ var start = new VirtualSnapshotPoint(snapshot.GetLineFromLineNumber(firstLineNumber + 1).Start);
+
+ return new Selection(
+ new VirtualSnapshotSpan(start, selection.End),
+ isReversed: selection.IsReversed);
+ }
+
+ private bool TryIndentBoxSelection()
+ {
+ return this.EditHelper((edit) =>
+ {
+ int tabSize = _editorOptions.GetTabSize();
+ int indentSize = _editorOptions.GetIndentSize();
+ bool convertTabsToSpaces = _editorOptions.IsConvertTabsToSpacesEnabled();
+
+ // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
+ // through a text change.
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxIndentForSelectionPoint(_multiSelectionBroker.BoxSelection.AnchorPoint, indentSize);
+ VirtualSnapshotPoint? activePoint = CalculateBoxIndentForSelectionPoint(_multiSelectionBroker.BoxSelection.ActivePoint, indentSize);
+
+ // Insert an indent for each portion of the selection (with an empty selection, there will only be a single
+ // span).
+ foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ {
+ if (InsertIndentForSpan(span, edit, exactlyOneIndentLevel: false) == null)
+ {
+ return false;
+ }
+ }
+
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+
+ return true;
+ });
}
/// <summary>
@@ -1661,58 +1877,158 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
/// </summary>
public bool Unindent()
{
- bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box &&
- _textView.Selection.Start != _textView.Selection.End;
+ if (UnindentWillCreateEdit())
+ {
+ Func<bool> action = () =>
+ {
+ using (_multiSelectionBroker.BeginBatchOperation())
+ {
+ var succeeded = this.EditHelper(edit =>
+ {
+ if (_multiSelectionBroker.IsBoxSelection)
+ {
+ return UnindentBoxSelection(edit);
+ }
+ else
+ {
+ return UnindentStreamSelections(edit);
+ }
+ });
- Func<bool> action = null;
+ _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll);
- if (_textView.Caret.InVirtualSpace && _textView.Selection.IsEmpty)
+ return succeeded;
+ }
+ };
+
+ return ExecuteAction(Strings.RemovePreviousTab, action, SelectionUpdate.Ignore, ensureVisible: true);
+ }
+ else
{
- this.MoveCaretToPreviousIndentStopInVirtualSpace();
+ using (_multiSelectionBroker.BeginBatchOperation())
+ {
+ if (_multiSelectionBroker.IsBoxSelection)
+ {
+ int columnsToRemove = DetermineMaxBoxUnindent();
+
+ // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
+ // through a text change.
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.AnchorPoint, columnsToRemove);
+ VirtualSnapshotPoint? activePoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.ActivePoint, columnsToRemove);
+
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+ }
+ else
+ {
+ _multiSelectionBroker.PerformActionOnAllSelections(transformer =>
+ {
+ VirtualSnapshotPoint point = GetPreviousIndentStopInVirtualSpace(transformer.Selection.Start);
+ transformer.MoveTo(point, select: false, PositionAffinity.Successor);
+ });
+ }
+ }
+
+ _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll);
return true;
}
- else if (!boxSelection && IndentOperationShouldBeMultiLine)
+ }
+
+ private bool UnindentWillCreateEdit()
+ {
+ var allSelections = _multiSelectionBroker.AllSelections;
+ if (_multiSelectionBroker.IsBoxSelection)
{
- action = () => PerformIndentActionOnEachBufferLine(RemoveIndentAtPoint);
+ // Box selection can only create an edit if at least one non-virtual subspan is of non-zero length.
+ for (int i = 0; i < allSelections.Count; i++)
+ {
+ var selection = allSelections[i];
+ if (!selection.Start.IsInVirtualSpace)
+ {
+ return true;
+ }
+ }
}
- else if (!boxSelection)
+ else
{
- action = () => EditHelper(edit => RemoveIndentAtPoint(_textView.Selection.Start.Position, edit, failOnNonWhitespaceCharacter: false));
+ // Stream selection can only create an edit if at least one selection has an end-point in non-virtual space.
+ for (int i = 0; i < allSelections.Count; i++)
+ {
+ var selection = allSelections[i];
+ if (!selection.Start.IsInVirtualSpace || !selection.End.IsInVirtualSpace)
+ {
+ return true;
+ }
+ }
}
- else // Box selection
+
+ return false;
+ }
+
+ private bool UnindentStreamSelections(ITextEdit edit)
+ {
+ bool succeeded = true;
+ _multiSelectionBroker.PerformActionOnAllSelections(transformer =>
{
- int columnsToRemove = DetermineMaxBoxUnindent();
+ Selection selection = transformer.Selection;
- action = () => EditHelper(edit =>
+ if (selection.IsEmpty && selection.InsertionPoint.IsInVirtualSpace)
{
- // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
- // through a text change.
- VirtualSnapshotPoint? anchorPoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.AnchorPoint, columnsToRemove);
- VirtualSnapshotPoint? activePoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.ActivePoint, columnsToRemove);
-
- // Remove an indent for each portion of the selection (with an empty selection, there will only be a single
- // span).
- foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ VirtualSnapshotPoint point = GetPreviousIndentStopInVirtualSpace(selection.InsertionPoint);
+ transformer.MoveTo(point, select: false, PositionAffinity.Successor);
+ }
+ else if (IndentOperationShouldBeMultiLine(selection))
+ {
+ var selections = new List<(Selection oldSelection, Selection newSelection)>();
+ if (PerformIndentActionOnEachBufferLine(edit, selections, selection, RemoveIndentAtPoint) == null)
{
- if (!RemoveIndentAtPoint(span.Start.Position, edit, failOnNonWhitespaceCharacter: false, columnsToRemove: columnsToRemove))
- return false;
+ succeeded = false;
+ return;
}
+ }
+ else
+ {
+ if (!RemoveIndentAtPoint(selection.Start.Position, edit, failOnNonWhitespaceCharacter: false))
+ {
+ succeeded = false;
+ return;
+ }
+ }
+ });
- FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+ return succeeded;
+ }
- return true;
+ private bool UnindentBoxSelection(ITextEdit edit)
+ {
+ int columnsToRemove = DetermineMaxBoxUnindent();
- });
+ // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
+ // through a text change.
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.AnchorPoint, columnsToRemove);
+ VirtualSnapshotPoint? activePoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.ActivePoint, columnsToRemove);
+
+ // Remove an indent for each portion of the selection (with an empty selection, there will only be a single
+ // span).
+ foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ {
+ if (!RemoveIndentAtPoint(span.Start.Position, edit, failOnNonWhitespaceCharacter: false, columnsToRemove: columnsToRemove))
+ return false;
}
- return ExecuteAction(Strings.RemovePreviousTab, action, SelectionUpdate.Ignore, ensureVisible: true);
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+
+ return true;
}
public bool IncreaseLineIndent()
{
Func<bool> action = () =>
{
+ // NOTE: This method doesn't account for multiple multi-line selections indenting the same
+ // line, so using this method will double indent the line intersected by both selections.
+ // Not worth fixing at the moment because this isn't a mapped command. See Indent for an
+ // example of how this should work.
return PerformIndentActionOnEachBufferLine(InsertSingleIndentAtPoint);
};
@@ -2973,9 +3289,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
#endif
}
-#endregion // IEditorOperations Members
+ #endregion // IEditorOperations Members
-#region Virtual Space to Whitespace helpers
+ #region Virtual Space to Whitespace helpers
public string GetWhitespaceForVirtualSpace(VirtualSnapshotPoint point)
{
@@ -3064,9 +3380,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return textToInsert;
}
-#endregion
+ #endregion
-#region Text insertion helpers
+ #region Text insertion helpers
private bool InsertText(string text, bool final)
{
@@ -3502,9 +3818,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return succeeded;
}
-#endregion
+ #endregion
-#region Clipboard and RTF helpers
+ #region Clipboard and RTF helpers
private Func<bool> PrepareClipboardSelectionCopy()
{
@@ -3603,24 +3919,27 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
private string GenerateRtf(NormalizedSnapshotSpanCollection spans)
{
#if WINDOWS
- //Don't generate RTF for large spans (since it is expensive and probably not wanted).
- int length = spans.Sum((span) => span.Length);
- if (length < _textView.Options.GetOptionValue(MaxRtfCopyLength.OptionKey))
+ if (_textView.Options.GetOptionValue(EnableRtfCopy.OptionKey))
{
- if (_textView.Options.GetOptionValue(UseAccurateClassificationForRtfCopy.OptionKey))
+ //Don't generate RTF for large spans (since it is expensive and probably not wanted).
+ int length = spans.Sum((span) => span.Length);
+ if (length < _textView.Options.GetOptionValue(MaxRtfCopyLength.OptionKey))
{
- using (var dialog = WaitHelper.Wait(_factory.WaitIndicator, Strings.WaitTitle, Strings.WaitMessage))
+ if (_textView.Options.GetOptionValue(UseAccurateClassificationForRtfCopy.OptionKey))
{
- return ((IRtfBuilderService2)(_factory.RtfBuilderService)).GenerateRtf(spans, dialog.CancellationToken);
+ using (var dialog = WaitHelper.Wait(_factory.WaitIndicator, Strings.WaitTitle, Strings.WaitMessage))
+ {
+ return ((IRtfBuilderService2)(_factory.RtfBuilderService)).GenerateRtf(spans, dialog.CancellationToken);
+ }
+ }
+ else
+ {
+ return _factory.RtfBuilderService.GenerateRtf(spans);
}
}
- else
- {
- return _factory.RtfBuilderService.GenerateRtf(spans);
- }
- }
else
return null;
+ }
#else
return null;
@@ -3632,9 +3951,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return GenerateRtf(new NormalizedSnapshotSpanCollection(span));
}
-#endregion
+ #endregion
-#region Horizontal whitespace helpers
+ #region Horizontal whitespace helpers
private bool DeleteHorizontalWhitespace()
{
@@ -3816,22 +4135,60 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return startPoint;
}
-#endregion
+ #endregion
-#region Indent/unindent helpers
+ #region Indent/unindent helpers
// Perform the given indent action (indent/unindent) on each line at the first non-whitespace
// character, skipping lines that are either empty or just whitespace.
// This method is used by Indent, Unindent, IncreaseLineIndent, and DecreaseLineIndent, and
// is essentially a replacement for the editor primitive's Indent and Unindent functions.
- private bool PerformIndentActionOnEachBufferLine(Func<SnapshotPoint, ITextEdit, bool> action)
+ private bool PerformIndentActionOnEachBufferLine(Func<SnapshotPoint, ITextEdit, int?> action)
+ {
+ Func<bool> indentAction = () =>
+ {
+ var newSelections = new FrugalList<(Selection old, Selection newSel)>();
+ if (!this.EditHelper(edit =>
+ {
+ bool succeeded = true;
+
+ using (_multiSelectionBroker.BeginBatchOperation())
+ {
+ _multiSelectionBroker.PerformActionOnAllSelections(transformer =>
+ {
+ if (this.PerformIndentActionOnEachBufferLine(edit, newSelections, transformer.Selection, action) == null)
+ {
+ succeeded = false;
+ }
+ });
+ }
+
+ return succeeded;
+ }))
+ {
+ return false;
+ }
+
+ // Apply selection changes after the edit so we can apply the desired point tracking mode.
+ TransformSelections(newSelections);
+
+ _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll);
+
+ return true;
+ };
+
+ return this.ExecuteAction(Strings.IncreaseLineIndent, indentAction, SelectionUpdate.Ignore);
+ }
+
+ private int? PerformIndentActionOnEachBufferLine(ITextEdit textEdit, IList<(Selection old, Selection newSel)> newSelections, Selection selection, Func<SnapshotPoint, ITextEdit, int?> action)
{
- Func<ITextEdit, bool> editAction = edit =>
+ Func<ITextEdit, int?> editAction = edit =>
{
+ int? actionResult = null;
ITextSnapshot snapshot = _textView.TextSnapshot;
- int startLineNumber = snapshot.GetLineNumberFromPosition(_textView.Selection.Start.Position);
- int endLineNumber = snapshot.GetLineNumberFromPosition(_textView.Selection.End.Position);
+ int startLineNumber = snapshot.GetLineNumberFromPosition(selection.Start.Position);
+ int endLineNumber = snapshot.GetLineNumberFromPosition(selection.End.Position);
for (int i = startLineNumber; i <= endLineNumber; i++)
{
@@ -3840,7 +4197,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
// If the line is blank or the (non-empty) selection ends at the start of this line, exclude
// the line from processing.
if (line.Length == 0 ||
- (!_textView.Selection.IsEmpty && line.Start == _textView.Selection.End.Position))
+ (!selection.IsEmpty && line.Start == selection.End.Position))
continue;
TextPoint textPoint = _editorPrimitives.Buffer.GetTextPoint(line.Start).GetFirstNonWhiteSpaceCharacterOnLine();
@@ -3850,67 +4207,73 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
SnapshotPoint point = new SnapshotPoint(snapshot, textPoint.CurrentPosition);
- if (!action(point, edit))
- return false;
+ actionResult = action(point, edit);
+ if (actionResult == null)
+ {
+ return null;
+ }
}
- return true;
+ // Return the indent of the final line.
+ return actionResult;
};
// Track the start of the selection with PointTrackingMode.Negative through the text
// change. If the result has the same absolute snapshot offset as the point before the change, we'll move
// the selection start point back to there instead of letting it track automatically.
- ITrackingPoint startPoint = _textView.TextSnapshot.CreateTrackingPoint(_textView.Selection.Start.Position, PointTrackingMode.Negative);
- int startPositionBufferChange = _textView.Selection.Start.Position;
+ ITrackingPoint startPoint = _textView.TextSnapshot.CreateTrackingPoint(selection.Start.Position, PointTrackingMode.Negative);
+ int startPositionBufferChange = selection.Start.Position;
- if (!EditHelper(editAction))
- return false;
+ var result = editAction.Invoke(textEdit);
+ if (result == null)
+ {
+ return null;
+ }
VirtualSnapshotPoint newStart = new VirtualSnapshotPoint(startPoint.GetPoint(_textView.TextSnapshot));
if (newStart.Position == startPositionBufferChange)
{
- bool isReversed = _textView.Selection.IsReversed;
- VirtualSnapshotPoint anchor = isReversed ? _textView.Selection.End : newStart;
- VirtualSnapshotPoint active = isReversed ? newStart : _textView.Selection.End;
- SelectAndMoveCaret(anchor, active);
+ bool isReversed = selection.IsReversed;
+ VirtualSnapshotPoint anchor = isReversed ? selection.End : newStart;
+ VirtualSnapshotPoint active = isReversed ? newStart : selection.End;
+
+ newSelections.Add(
+ (selection,
+ new Selection(
+ active.TranslateTo(_textView.TextSnapshot, PointTrackingMode.Positive),
+ anchor.TranslateTo(_textView.TextSnapshot, PointTrackingMode.Negative),
+ active.TranslateTo(_textView.TextSnapshot, PointTrackingMode.Positive),
+ selection.InsertionPointAffinity)));
}
- return true;
+ return result;
}
- // This is used by indent/unindent should be multiline operations. To be multiline, the selection
- // points must be on separate lines, and not just an entire line (though not the entire last line, which
- // we special case). This is for backwards compatibility with Orcas, but is generally undesirable behavior.
- // Dev10 #856382 tracks removing this special behavior for indent and just treating it like a tab at the
- // start of the selection.
- private bool IndentOperationShouldBeMultiLine
+ private bool IndentOperationShouldBeMultiLine(Selection selection)
{
- get
- {
- if (_textView.Selection.IsEmpty)
- return false;
+ if (selection.IsEmpty)
+ return false;
- var startLine = _textView.Selection.Start.Position.GetContainingLine();
+ var startLine = selection.Start.Position.GetContainingLine();
- bool pointsOnSameLine = _textView.Selection.End.Position <= startLine.End;
+ bool pointsOnSameLine = selection.End.Position <= startLine.End;
- bool lastLineOfFile = startLine.End == startLine.EndIncludingLineBreak;
- bool entireLastLineSelected = lastLineOfFile &&
- _textView.Selection.Start.Position == startLine.Start &&
- _textView.Selection.End.Position == startLine.End;
+ bool lastLineOfFile = startLine.End == startLine.EndIncludingLineBreak;
+ bool entireLastLineSelected = lastLineOfFile &&
+ _textView.Selection.Start.Position == startLine.Start &&
+ _textView.Selection.End.Position == startLine.End;
- return !pointsOnSameLine || entireLastLineSelected;
- }
+ return !pointsOnSameLine || entireLastLineSelected;
}
- private bool InsertSingleIndentAtPoint(SnapshotPoint point, ITextEdit edit)
+ private int? InsertSingleIndentAtPoint(SnapshotPoint point, ITextEdit edit)
{
VirtualSnapshotPoint virtualPoint = new VirtualSnapshotPoint(point);
VirtualSnapshotSpan span = new VirtualSnapshotSpan(virtualPoint, virtualPoint);
- return InsertIndentForSpan(span, edit, exactlyOneIndentLevel: true, useBufferPrimitives: true);
+ return InsertIndentForSpan(span, edit, exactlyOneIndentLevel: true, useBufferPrimitives: true) != null ? 0 : default(int?);
}
- private bool InsertIndentForSpan(VirtualSnapshotSpan span, ITextEdit edit, bool exactlyOneIndentLevel, bool useBufferPrimitives = false)
+ private int? InsertIndentForSpan(VirtualSnapshotSpan span, ITextEdit edit, bool exactlyOneIndentLevel, bool useBufferPrimitives = false, int columnOffset = 0)
{
int indentSize = _textView.Options.GetIndentSize();
bool convertTabsToSpaces = _textView.Options.IsConvertTabsToSpacesEnabled();
@@ -3923,7 +4286,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
// In a box selection, we don't insert anything for lines in virtual space
if (boxSelection && point.IsInVirtualSpace)
{
- return true;
+ return 0;
}
string textToInsert;
@@ -3972,15 +4335,13 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
else
textPoint = _editorPrimitives.View.GetTextPoint(point.Position.Position);
- currentColumn = textPoint.Column + point.VirtualSpaces;
+ currentColumn = textPoint.Column + point.VirtualSpaces + columnOffset;
if (exactlyOneIndentLevel)
distanceToNextIndentStop = indentSize;
else
distanceToNextIndentStop = indentSize - (currentColumn % indentSize);
- int columnToInsertTo = currentColumn + distanceToNextIndentStop;
-
textToInsert = GetWhiteSpaceForPositionAndVirtualSpace(point.Position, point.VirtualSpaces + distanceToNextIndentStop, useBufferPrimitives);
}
@@ -3999,12 +4360,15 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
}
}
- return edit.Replace(Span.FromBounds(startPointForReplace, endPointForReplace), textToInsert);
+ var spanToReplace = Span.FromBounds(startPointForReplace, endPointForReplace);
+ int newColumnOffset = (textToInsert.Length - spanToReplace.Length) + columnOffset;
+
+ return edit.Replace(spanToReplace, textToInsert) ? newColumnOffset : default(int?);
}
- private bool RemoveIndentAtPoint(SnapshotPoint point, ITextEdit edit)
+ private int? RemoveIndentAtPoint(SnapshotPoint point, ITextEdit edit)
{
- return RemoveIndentAtPoint(point, edit, failOnNonWhitespaceCharacter: true, useBufferPrimitives: true);
+ return RemoveIndentAtPoint(point, edit, failOnNonWhitespaceCharacter: true, useBufferPrimitives: true) ? 0 : default(int?);
}
/// <summary>
@@ -4088,14 +4452,6 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return edit.Replace(Span.FromBounds(startPointForReplace, point.Position), textToInsert);
}
- private void MoveCaretToPreviousIndentStopInVirtualSpace()
- {
- Debug.Assert(_textView.Caret.InVirtualSpace);
-
- VirtualSnapshotPoint point = GetPreviousIndentStopInVirtualSpace(_textView.Caret.Position.VirtualBufferPosition);
- _textView.Caret.MoveTo(point);
- }
-
/// <summary>
/// Used by the un-indenting logic to determine what an unindent means in virtual space.
/// </summary>
@@ -4118,9 +4474,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return new VirtualSnapshotPoint(point.Position);
}
-#endregion
+ #endregion
-#region Box Selection indent/unindent helpers
+ #region Box Selection indent/unindent helpers
/// <summary>
/// Given a "fix-up" anchor/active point determined before the box operation, fix up the current selection's
@@ -4234,9 +4590,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return maxColumnUnindent;
}
-#endregion
+ #endregion
-#region Miscellaneous line helpers
+ #region Miscellaneous line helpers
private DisplayTextRange GetFullLines()
{
@@ -4300,9 +4656,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return firstTextColumn.CurrentPosition == displayTextPoint.EndOfViewLine;
}
-#endregion
+ #endregion
-#region Tabs <-> spaces
+ #region Tabs <-> spaces
private bool ConvertSpacesAndTabsHelper(bool toTabs)
{
@@ -4427,9 +4783,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return true;
}
-#endregion
+ #endregion
-#region Edit/Replace/Delete helpers
+ #region Edit/Replace/Delete helpers
internal bool EditHelper(Func<ITextEdit, bool> editAction)
{
@@ -4506,7 +4862,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
});
}
-#endregion
+ #endregion
internal bool IsEmptyBoxSelection()
{
@@ -4893,6 +5249,21 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
}
[Export(typeof(EditorOptionDefinition))]
+ [Name(EnableRtfCopy.OptionName)]
+ public sealed class EnableRtfCopy : EditorOptionDefinition<bool>
+ {
+ public const string OptionName = "EnableRtfCopy";
+ public static readonly EditorOptionKey<bool> OptionKey = new EditorOptionKey<bool>(EnableRtfCopy.OptionName);
+
+ public override bool Default { get { return true; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<bool> Key { get { return EnableRtfCopy.OptionKey; } }
+ }
+
+ [Export(typeof(EditorOptionDefinition))]
[Name(UseAccurateClassificationForRtfCopy.OptionName)]
public sealed class UseAccurateClassificationForRtfCopy : EditorOptionDefinition<bool>
{
diff --git a/src/Text/Impl/EditorOptions/EditorOptions.cs b/src/Text/Impl/EditorOptions/EditorOptions.cs
index cb97410..b327faf 100644
--- a/src/Text/Impl/EditorOptions/EditorOptions.cs
+++ b/src/Text/Impl/EditorOptions/EditorOptions.cs
@@ -11,28 +11,30 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
- using System.Linq;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Utilities;
using Microsoft.VisualStudio.Utilities;
internal class EditorOptions : IEditorOptions
{
- IPropertyOwner Scope { get; set; }
+ internal IPropertyOwner Scope { get; private set; }
HybridDictionary OptionsSetLocally { get; set; }
private EditorOptionsFactoryService _factory;
+ internal readonly bool AllowsLateBinding;
FrugalList<WeakReference> DerivedEditorOptions = new FrugalList<WeakReference>();
internal EditorOptions(EditorOptions parent,
IPropertyOwner scope,
- EditorOptionsFactoryService factory)
+ EditorOptionsFactoryService factory,
+ bool allowsLateBinding = false)
{
_parent = parent;
_factory = factory;
this.Scope = scope;
+ this.AllowsLateBinding = allowsLateBinding;
this.OptionsSetLocally = new HybridDictionary();
@@ -250,6 +252,11 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
#endregion
+ internal void SetScope(IPropertyOwner newScope)
+ {
+ this.Scope = newScope;
+ }
+
#region Private Helpers
//A hook so we can tell whether or not the options have been hooked. Used only by unit tests.
diff --git a/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs
index a7f2686..e21ce2b 100644
--- a/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs
+++ b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs
@@ -18,7 +18,8 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
using Microsoft.VisualStudio.Utilities;
[Export(typeof(IEditorOptionsFactoryService))]
- internal sealed class EditorOptionsFactoryService : IEditorOptionsFactoryService
+ [Export(typeof(IEditorOptionsFactoryService2))]
+ internal sealed class EditorOptionsFactoryService : IEditorOptionsFactoryService2
{
[ImportMany(typeof(EditorOptionDefinition))]
internal List<Lazy<EditorOptionDefinition, INameMetadata>> OptionImports { get; set; }
@@ -31,7 +32,6 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
internal GuardedOperations guardedOperations = null;
#region IEditorOptionsFactoryService Members
-
public IEditorOptions GetOptions(IPropertyOwner scope)
{
if (scope == null)
@@ -42,7 +42,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
public IEditorOptions CreateOptions()
{
- return new EditorOptions(this.GlobalOptions as EditorOptions, null, this);
+ return this.CreateOptions(allowsLateBinding: false);
}
public IEditorOptions GlobalOptions
@@ -51,7 +51,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
{
if (_globalOptions == null)
{
- //We're guranteed that the first thing that happens when anyone tries to create options is that the global options will be created first,
+ //We're guaranteed that the first thing that happens when anyone tries to create options is that the global options will be created first,
//so do initialization here.
_globalOptions = new EditorOptions(null, null, this);
@@ -65,6 +65,30 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
}
#endregion
+ #region IEditorOptionsFactoryService2 Members
+ public bool TryBindToScope(IEditorOptions options, IPropertyOwner scope)
+ {
+ if (scope == null)
+ throw new ArgumentNullException(nameof(scope));
+
+ var editorOptions = options as EditorOptions;
+ if ((editorOptions == null) || (!editorOptions.AllowsLateBinding) || scope.Properties.ContainsProperty(typeof(IEditorOptions)))
+ {
+ // options cannot be bound to the specified scope.
+ return false;
+ }
+
+ editorOptions.SetScope(scope);
+ scope.Properties.AddProperty(typeof(IEditorOptions), options);
+ return true;
+ }
+
+ public IEditorOptions CreateOptions(bool allowsLateBinding)
+ {
+ return new EditorOptions(this.GlobalOptions as EditorOptions, null, this, allowsLateBinding: allowsLateBinding);
+ }
+ #endregion
+
private void Initialize()
{
//Don't need to start locking things (yet)
@@ -108,7 +132,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
}
}
- internal EditorOptionDefinition GetOptionDefinition(string optionId)
+ public EditorOptionDefinition GetOptionDefinition(string optionId)
{
lock (_instantiatedOptionDefinitions)
{
diff --git a/src/Text/Impl/Outlining/OutliningManager.cs b/src/Text/Impl/Outlining/OutliningManager.cs
index 28decbe..6118d72 100644
--- a/src/Text/Impl/Outlining/OutliningManager.cs
+++ b/src/Text/Impl/Outlining/OutliningManager.cs
@@ -398,23 +398,19 @@ namespace Microsoft.VisualStudio.Text.Outlining
foreach (var tagSpan in tagSpans)
{
- var spans = tagSpan.Span.GetSpans(current);
-
- // We only accept this tag if it hasn't been split into multiple spans and if
- // it hasn't had pieces cut out of it from projection. Also, refuse 0-length
- // tags, as they wouldn't be hiding anything.
- if (spans.Count == 1 &&
- spans[0].Length > 0 &&
- spans[0].Length == tagSpan.Span.GetSpans(tagSpan.Span.AnchorBuffer)[0].Length)
+ // Attempt to map this tag up to the top level buffer in a contiguous fashion,
+ // rejecting it if either the start or end fails to map.
+ if (TryContiguousMapToSnapshot(tagSpan, current, out var mappedSpan) &&
+ (mappedSpan.Length > 0))
{
- ITrackingSpan trackingSpan = current.CreateTrackingSpan(spans[0], SpanTrackingMode.EdgeExclusive);
+ ITrackingSpan trackingSpan = current.CreateTrackingSpan(mappedSpan, SpanTrackingMode.EdgeExclusive);
var collapsible = new Collapsible(trackingSpan, tagSpan.Tag);
if (collapsibles.ContainsKey(collapsible))
{
// TODO: Notify providers somehow.
// Or rewrite so that such things are legal.
#if false
- Debug.WriteLine("IGNORING TAG " + spans[0] + " due to span conflict");
+ Debug.WriteLine("IGNORING TAG " + mappedSpan + " due to span conflict");
#endif
}
else
@@ -425,7 +421,7 @@ namespace Microsoft.VisualStudio.Text.Outlining
else
{
#if false
- Debug.WriteLine("IGNORING TAG " + tagSpan.Span.GetSpans(editBuffer) + " because it was split or shortened by projection");
+ Debug.WriteLine("IGNORING TAG " + tagSpan.Span.GetSpans(editBuffer) + " because its start or endpoint failed to map");
#endif
}
}
@@ -433,6 +429,33 @@ namespace Microsoft.VisualStudio.Text.Outlining
return collapsibles;
}
+ /// <summary>
+ /// Alternative to <see cref="IMappingSpan.GetSpans(ITextSnapshot)"/> that maps the span
+ /// as a contiguous unit so that spans are not split by nested projections.
+ /// </summary>
+ /// <remarks>
+ /// Spans fail to contiguously map if one or more of their end points do not exist in that
+ /// snapshot/buffer.
+ /// </remarks>
+ private static bool TryContiguousMapToSnapshot(IMappingTagSpan<IOutliningRegionTag> tagSpan, ITextSnapshot snapshot, out SnapshotSpan span)
+ {
+ var startPoint = tagSpan.Span.Start.GetPoint(snapshot, PositionAffinity.Successor);
+ if (startPoint.HasValue)
+ {
+ var endPoint = tagSpan.Span.End.GetPoint(snapshot, PositionAffinity.Successor);
+ if (endPoint.HasValue)
+ {
+ span = new SnapshotSpan(
+ startPoint.Value,
+ endPoint.Value);
+ return true;
+ }
+ }
+
+ span = default;
+ return false;
+ }
+
private IEnumerable<ICollapsible> MergeRegions(IEnumerable<ICollapsed> currentCollapsed, IEnumerable<ICollapsible> newCollapsibles,
out IEnumerable<ICollapsed> removedRegions)
{
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs b/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs
index 0cf9b54..8fa180f 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs
@@ -40,7 +40,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
_selectionTransformers.Add(_primaryTransformer);
// Ignore normal text structure navigators and take the plain text version to keep ownership of word navigation.
- _textStructureNavigator = Factory.TextStructureNavigatorSelectorService.CreateTextStructureNavigator(_textView.TextViewModel.EditBuffer, Factory.ContentTypeRegistryService.GetContentType("text"));
+ _textStructureNavigator = Factory.TextStructureNavigatorSelectorService.GetTextStructureNavigator(_textView.TextViewModel.EditBuffer);
_textView.LayoutChanged += OnTextViewLayoutChanged;
_textView.Closed += OnTextViewClosed;
@@ -60,9 +60,32 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
private void OnTextViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
- if (CurrentSnapshot != e.NewSnapshot)
+ using (var batchOp = BeginBatchOperation())
{
- CurrentSnapshot = e.NewSnapshot;
+ // If we get a text change, we need to go through all the selections and update them to be in the
+ // new snapshot. If there is just a visual change, we could still need to update selections because
+ // word wrap or collapsed regions might have moved around.
+ if (CurrentSnapshot != e.NewSnapshot)
+ {
+ CurrentSnapshot = e.NewSnapshot;
+ }
+ else if (e.NewViewState.VisualSnapshot != e.OldViewState.VisualSnapshot)
+ {
+ // Box selection is special. Moving _boxSelection is easy, but InnerSetBoxSelection will totally
+ // reset all the selections. It's easier to go a different path here than it is to special case
+ // NormalizeSelections, which is also called when adding an individual selection.
+ if (IsBoxSelection)
+ {
+ // MapToSnapshot does take the visual buffer into account as well. Calling it here should do the right thing
+ // for collapsed regions and word wrap.
+ _boxSelection.Selection = _boxSelection.Selection.MapToSnapshot(_currentSnapshot, _textView);
+ InnerSetBoxSelection();
+ }
+ else
+ {
+ NormalizeSelections(true);
+ }
+ }
}
}
@@ -197,19 +220,23 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
- internal void QueueCaretUpdatedEvent(SelectionTransformer selection)
+ internal void QueueCaretUpdatedEvent(SelectionTransformer transformer)
{
// This is set for calls to TransformSelection which doesn't
// make permanent changes to _selectionTransformers.
- if (selection == _standaloneTransformation)
+ if (transformer == _standaloneTransformation)
{
return;
}
- if (selection == _boxSelection)
+ if (transformer == _boxSelection)
{
InnerSetBoxSelection();
}
+ else
+ {
+ transformer.ModifiedByCurrentOperation = true;
+ }
_fireEvents = true;
FireSessionUpdatedIfNotBatched();
@@ -382,6 +409,9 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
private void FireSessionUpdated()
{
+ var changesFromNormalization = NormalizeSelections();
+ _fireEvents = _fireEvents || changesFromNormalization;
+
// Perform merges as late as possible so that each region can act independently for operations.
MergeSelections();
@@ -400,6 +430,42 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
+ /// <summary>
+ /// Takes selections and makes sure they do not occupy space within text elements like collapsed regions or multi-byte
+ /// characters.
+ /// </summary>
+ /// <param name="overrideModifiedFlags">If specified, ignores dirty flags and normalizes everything.</param>
+ /// <returns>True if anything changed, false otherwise.</returns>
+ private bool NormalizeSelections(bool overrideModifiedFlags = false)
+ {
+ bool selectionsChanged = false;
+
+ // Normalizing for box selection is a more drastic action, and needs to be done with perf in mind since it can throw away,
+ // and recreate large numbers of selections.
+ if (_boxSelection == null)
+ {
+ for (int i = 0; i < _selectionTransformers.Count; i++)
+ {
+ if (overrideModifiedFlags || _selectionTransformers[i].ModifiedByCurrentOperation)
+ {
+ _selectionTransformers[i].ModifiedByCurrentOperation = false;
+
+ // Mapping to the current snapshot has the side-affect of moving points away from the middle of text elements,
+ // or in other words collapsed regions and multi-byte characters.
+ var normalizedSelection = _selectionTransformers[i].Selection.MapToSnapshot(_currentSnapshot, _textView);
+
+ if (_selectionTransformers[i].Selection != normalizedSelection)
+ {
+ selectionsChanged = true;
+ _selectionTransformers[i].Selection = normalizedSelection;
+ }
+ }
+ }
+ }
+
+ return selectionsChanged;
+ }
+
private IFeatureService FeatureService
{
get
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs b/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs
index 276dd02..f65fa36 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs
@@ -2,12 +2,11 @@
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Operations;
+using Microsoft.VisualStudio.Text.Outlining;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
@@ -30,6 +29,9 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
[Import]
internal ITextSearchNavigatorFactoryService TextSearchNavigatorFactoryService { get; set; }
+ [Import]
+ internal IOutliningManagerService OutliningManagerService { get; set; }
+
public string DisplayName => Strings.MultiSelectionCancelCommandName;
public CommandState GetCommandState(EscapeKeyCommandArgs args) => CommandState.Available;
@@ -58,7 +60,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
public CommandState GetCommandState(RemoveLastSecondaryCaretCommandArgs args)
{
var broker = args.TextView.GetMultiSelectionBroker();
- return broker.HasMultipleSelections ? CommandState.Available : CommandState.Unavailable;
+ return broker.HasMultipleSelections || broker.PrimarySelection.Extent.Length > 0 ? CommandState.Available : CommandState.Unavailable;
}
public CommandState GetCommandState(MoveLastCaretDownCommandArgs args)
@@ -113,7 +115,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (result == true)
{
- broker.TryEnsureVisible(newPrimary, EnsureSpanVisibleOptions.AlwaysCenter);
+ broker.TryEnsureVisible(newPrimary, EnsureSpanVisibleOptions.None);
}
return result;
@@ -140,14 +142,14 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (result == true)
{
- broker.TryEnsureVisible(newPrimary, EnsureSpanVisibleOptions.AlwaysCenter);
+ broker.TryEnsureVisible(newPrimary, EnsureSpanVisibleOptions.None);
}
return result;
}
private static Selection InsertDiscoveredMatchRegion(IMultiSelectionBroker broker, Selection primaryRegion, SnapshotSpan found)
- {
+ {
var newSpan = new VirtualSnapshotSpan(found);
var newSelection = new Selection(newSpan, primaryRegion.IsReversed);
broker.AddSelection(newSelection);
@@ -161,6 +163,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (broker.PrimarySelection.IsEmpty)
{
broker.TryPerformActionOnSelection(broker.PrimarySelection, PredefinedSelectionTransformations.SelectCurrentWord, out _);
+ return true;
}
var navigator = TextSearchNavigatorFactoryService.CreateSearchNavigator(args.TextView.TextViewModel.EditBuffer);
@@ -211,8 +214,14 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (!broker.SelectedSpans.OverlapsWith(found))
{
+ var outliningManager = OutliningManagerService.GetOutliningManager(args.TextView);
+ if (outliningManager != null)
+ {
+ outliningManager.ExpandAll(found, collapsible => true);
+ }
+
var addedRegion = InsertDiscoveredMatchRegion(broker, primaryRegion, found);
- broker.TryEnsureVisible(addedRegion, EnsureSpanVisibleOptions.AlwaysCenter);
+ broker.TryEnsureVisible(addedRegion, EnsureSpanVisibleOptions.None);
return true;
}
@@ -249,6 +258,9 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
navigator.Find(); // Get and ignore the primary region
+ var newlySelectedSpans = new List<SnapshotSpan>();
+ var oldSelectedSpans = broker.SelectedSpans;
+
while (navigator.Find())
{
var found = navigator.CurrentResult.Value;
@@ -259,12 +271,45 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
break;
}
- if (!broker.SelectedSpans.OverlapsWith(found))
+ if (!oldSelectedSpans.OverlapsWith(found))
+ {
+ newlySelectedSpans.Add(found);
+ }
+ }
+
+ // Make sure that none of the newly selected spans overlap
+ for(int i = 0; i < (newlySelectedSpans.Count - 1); i++)
+ {
+ if (newlySelectedSpans[i].OverlapsWith(newlySelectedSpans[i+1]))
{
- InsertDiscoveredMatchRegion(broker, primaryRegion, found);
+ newlySelectedSpans.RemoveAt(i + 1);
+
+ // decrement 1 so we can compare i and what used to be i+2 next time
+ i--;
}
}
+ var newlySelectedSpanCollection = new NormalizedSnapshotSpanCollection(newlySelectedSpans);
+
+ // Ok, we've figured out what selections we want to add. Now we need to expand any outlining regions before finally adding the selections
+ var outliningManager = OutliningManagerService.GetOutliningManager(args.TextView);
+ if (outliningManager != null)
+ {
+ var extent = new SnapshotSpan(
+ newlySelectedSpanCollection[0].Start,
+ newlySelectedSpanCollection[newlySelectedSpanCollection.Count - 1].End);
+ outliningManager.ExpandAll(extent, collapsible =>
+ {
+ return newlySelectedSpanCollection.IntersectsWith(collapsible.Extent.GetSpan(broker.CurrentSnapshot));
+ });
+ }
+
+ // Yay, we can finally actually add the selections
+ for (int i = 0; i < newlySelectedSpans.Count; i++)
+ {
+ broker.AddSelectionRange(newlySelectedSpans.Select(span => new Selection(span, broker.PrimarySelection.IsReversed)));
+ }
+
return true;
}
@@ -298,9 +343,13 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (broker.TryRemoveSelection(toRemove))
{
- return broker.TryEnsureVisible(FindLastSelection(broker), EnsureSpanVisibleOptions.AlwaysCenter);
+ return broker.TryEnsureVisible(FindLastSelection(broker), EnsureSpanVisibleOptions.None);
}
}
+ else if (broker.PrimarySelection.Extent.Length > 0)
+ {
+ broker.TryPerformActionOnSelection(broker.PrimarySelection, PredefinedSelectionTransformations.ClearSelection, out _);
+ }
return false;
}
@@ -317,7 +366,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
{
if (broker.TryRemoveSelection(toRemove))
{
- return broker.TryEnsureVisible(FindLastSelection(broker), EnsureSpanVisibleOptions.AlwaysCenter);
+ return broker.TryEnsureVisible(FindLastSelection(broker), EnsureSpanVisibleOptions.None);
}
}
}
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs b/src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs
index d4bc06e..e80770c 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs
@@ -204,6 +204,15 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
+ /// <summary>
+ /// Determines whether this transformer should be considered as changed by the current operation. This is used
+ /// by normalization code which checks after all operations have completed to ensure that we don't leave a caret
+ /// or selection sitting in the middle of a collapsed region or multi-byte character. It starts as true so that we
+ /// check by default for all new transformers. We need this because the actual normalizing check does expensive formatting
+ /// which we'd like to avoid if possible for selections that were not changed by an operation.
+ /// </summary>
+ internal bool ModifiedByCurrentOperation { get; set; } = true;
+
private static SnapshotPoint GetFirstNonWhiteSpaceCharacterInSpan(SnapshotSpan span)
{
var toReturn = span.Start;
@@ -226,7 +235,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
return (endOfWord == endOfLine) && (endOfLine > currentPosition);
}
- private static bool IsSpanABlankLine(SnapshotSpan currentWord, ITextViewLine currentLine)
+ private static bool IsSpanABlankLine(SnapshotSpan currentWord, ITextSnapshotLine currentLine)
{
return currentWord.IsEmpty && currentWord == currentLine.Extent;
}
@@ -236,65 +245,106 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
/// </summary>
/// <param name="previousWord">The current previous word.</param>
/// <param name="line">The line containing previous word.</param>
+ private static bool ShouldContinuePastPreviousWord(SnapshotSpan previousWord, ITextSnapshotLine line)
+ {
+ // If the previous word is whitespace, and the previous word is not the start of the line
+ // then it should be included in the previous word.
+ return char.IsWhiteSpace(previousWord.Snapshot[previousWord.Start]) && previousWord.Start != line.Start;
+ }
+
+ private SnapshotPoint NextCharacter(SnapshotPoint current)
+ {
+ if (current.Snapshot.Length == current.Position)
+ {
+ return current;
+ }
+
+ return _broker.TextView.GetTextElementSpan(current).End;
+ }
- private static bool ShouldContinuePastPreviousWord(SnapshotSpan previousWord, ITextViewLine line)
+ private SnapshotPoint PreviousCharacter(SnapshotPoint current)
{
- return char.IsWhiteSpace(previousWord.Snapshot[previousWord.Start]) && !IsSpanABlankLine(previousWord, line);
+ if (current.Position == 0)
+ {
+ return current;
+ }
+
+ return _broker.TextView.GetTextElementSpan(current - 1).Start;
}
private SnapshotSpan GetNextWord()
{
var currentPosition = _selection.InsertionPoint.Position;
var currentWord = _broker.TextStructureNavigator.GetExtentOfWord(currentPosition);
- var currentLine = _broker.TextView.GetTextViewLineContainingBufferPosition(currentPosition);
+ var currentLine = currentPosition.GetContainingLine();
if (currentWord.Span.End < _currentSnapshot.Length)
{
- // If the current point is at the end of the line, look for the next word on the next line
- if (_selection.InsertionPoint.Position == currentLine.End)
+ // Get the current caret position
+ int startPosition = currentPosition.Position;
+ int endOfLine = currentLine.End.Position;
+
+ if (startPosition >= endOfLine)
{
- var startOfNextLine = currentLine.EndIncludingLineBreak;
- var nextLine = _broker.TextView.GetTextViewLineContainingBufferPosition(startOfNextLine);
- var wordOnNextLine = _broker.TextStructureNavigator.GetExtentOfWord(startOfNextLine);
+ // Move the caret to the next line since it is at the end of the current line
+ currentPosition = currentLine.EndIncludingLineBreak;
+ currentLine = currentPosition.GetContainingLine();
+ endOfLine = currentLine.End.Position;
- if (wordOnNextLine.IsSignificant)
+ // Move past whitespace on the next line
+ while (currentPosition.Position < endOfLine && char.IsWhiteSpace(currentPosition.Snapshot[currentPosition.Position]))
{
- return wordOnNextLine.Span;
- }
- else if (wordOnNextLine.Span.End >= nextLine.End)
- {
- return new SnapshotSpan(nextLine.End, nextLine.End);
+ currentPosition = NextCharacter(currentPosition);
}
- // Simulate continuing a search on the next line recursively by just updating our starting point and falling through.
- currentPosition = startOfNextLine;
- currentWord = wordOnNextLine;
- currentLine = nextLine;
+ return new SnapshotSpan(CurrentSnapshot, currentPosition.Position, 0);
}
- // By default, VS stops at line breaks when determing word
- // boundaries.
- if (ShouldStopAtEndOfLine(currentWord.Span.End, currentLine.End, currentPosition) || IsSpanABlankLine(currentWord.Span, currentLine))
+ currentPosition = NextCharacter(currentPosition);
+
+ // If we are at the end of the line, stop looking for the next word - we want the caret to
+ // stop at the end of each line.
+ if (currentPosition >= endOfLine)
{
- return new SnapshotSpan(currentWord.Span.End, currentWord.Span.End);
+ return new SnapshotSpan(CurrentSnapshot, currentPosition.Position, 0);
}
- var nextWord = _broker.TextStructureNavigator.GetExtentOfWord(currentWord.Span.End);
-
- if (!nextWord.IsSignificant)
+ // Skip past whitespace.
+ while (currentPosition < endOfLine && char.IsWhiteSpace(currentPosition.Snapshot[currentPosition.Position]))
{
- nextWord = _broker.TextStructureNavigator.GetExtentOfWord(nextWord.Span.End);
+ currentPosition = NextCharacter(currentPosition);
}
- int start = nextWord.Span.Start;
- int end = nextWord.Span.End;
+ // If the position is still not at the end of the line get the current
+ // word.
+ if (currentPosition < endOfLine)
+ {
+ currentWord = _broker.TextStructureNavigator.GetExtentOfWord(currentPosition);
- // The text structure navigator can return a word with whitespace attached at the end.
- // Handle that case here.
- start = Math.Max(start, currentWord.Span.End);
- int length = end - start;
+ if (currentWord.Span.Start < currentPosition)
+ {
+ // If the current word starts before the current position, move to the end of the
+ // current word.
+ currentPosition = currentWord.Span.End;
+ }
+
+ bool hitWhitespace = false;
+ // Skip past whitespace at the end of the word.
+ while (currentPosition < endOfLine && char.IsWhiteSpace(currentPosition.Snapshot[currentPosition.Position]))
+ {
+ hitWhitespace = true;
+ currentPosition = NextCharacter(currentPosition);
+ }
- return new SnapshotSpan(_currentSnapshot, start, length);
+ return hitWhitespace ? new SnapshotSpan(CurrentSnapshot, currentPosition, 0) : _broker.TextStructureNavigator.GetExtentOfWord(currentPosition).Span;
+ }
+ else if (currentPosition == endOfLine)
+ {
+ // Just wrap one line be done
+ currentPosition = NextCharacter(currentPosition);
+ }
+
+ return new SnapshotSpan(currentPosition, currentPosition);
}
return new SnapshotSpan(_currentSnapshot, _currentSnapshot.Length, 0);
@@ -304,45 +354,75 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
{
var position = _selection.InsertionPoint.Position;
var word = _broker.TextStructureNavigator.GetExtentOfWord(position);
- var line = _broker.TextView.GetTextViewLineContainingBufferPosition(position);
+ var line = position.GetContainingLine();
- if (word.Span.Start > 0)
+ if (word.Span.Start == 0)
{
- // By default, VS stops at line breaks when determing word
- // boundaries.
- if ((word.Span.Start == line.Start) &&
- (position != line.Start || _selection.InsertionPoint.IsInVirtualSpace))
- {
- return new SnapshotSpan(line.Start, line.Start);
- }
+ // We can't move back anymore, just give the start of the document as an empty span.
+ return new SnapshotSpan(_currentSnapshot, 0, 0);
+ }
+
+ if (word.Span.Start == line.Start && !_selection.InsertionPoint.IsInVirtualSpace)
+ {
+ // We're starting at the beginning of the line, jump to the end of the previous line, then continute the algorithm as normal.
+ var lineNumber = line.LineNumber;
+ line = _broker.CurrentSnapshot.GetLineFromLineNumber(Math.Max(0, lineNumber - 1));
+ position = line.End;
- // If the point is not at the beginning of a word that is not whitespace, it is possible
- // that the "current word" is also the word we wish to navigate to the beginning of.
- if ((word.Span.Start != position) &&
- (!word.Span.IsEmpty))
+ // If the line is empty, just return
+ if (line.Extent.IsEmpty)
{
- return word.Span;
+ return line.Extent;
}
- // Ok, we have to start moving backwards through the document.
- position = word.Span.Start - 1;
+ // Make sure we're not in the middle of a text element like a hidden region, a multi-byte char or something else weird.
+ position = _broker.TextView.GetTextElementSpan(position).Start;
word = _broker.TextStructureNavigator.GetExtentOfWord(position);
- line = _broker.TextView.GetTextViewLineContainingBufferPosition(position);
+ }
- if (word.Span.Start > 0)
- {
- if (ShouldContinuePastPreviousWord(word.Span, line))
- {
- // Move back one more time
- position = word.Span.Start - 1;
- word = _broker.TextStructureNavigator.GetExtentOfWord(position);
- }
- }
+ // By default, VS stops at line breaks when determing word
+ // boundaries.
+ if ((word.Span.Start == line.Start) &&
+ (position != line.Start || _selection.InsertionPoint.IsInVirtualSpace))
+ {
+ return new SnapshotSpan(line.Start, line.Start);
+ }
+ // If the point is not at the beginning of a word that is not whitespace, it is possible
+ // that the "current word" is also the word we wish to navigate to the beginning of.
+ if ((word.Span.Start != position) && (!word.Span.IsEmpty) && word.IsSignificant)
+ {
return word.Span;
}
- return new SnapshotSpan(_currentSnapshot, 0, 0);
+ // Ok, we have to start moving backwards through the document.
+ do
+ {
+ position = PreviousCharacter(position);
+
+ // Skip past whitespace at the start of the word.
+ } while (position > line.Start && char.IsWhiteSpace(position.GetChar()));
+
+ // Again, VS stops at line breaks when determining word boundaries
+ if (position <= line.Start)
+ {
+ return new SnapshotSpan(position, position);
+ }
+
+ word = _broker.TextStructureNavigator.GetExtentOfWord(position);
+ line = position.GetContainingLine();
+
+ if (word.Span.Start > 0)
+ {
+ if (ShouldContinuePastPreviousWord(word.Span, line))
+ {
+ // Move back one more time
+ position = PreviousCharacter(position);
+ word = _broker.TextStructureNavigator.GetExtentOfWord(position);
+ }
+ }
+
+ return word.Span;
}
public void MoveTo(VirtualSnapshotPoint point, bool select, PositionAffinity insertionPointAffinity)
@@ -354,13 +434,52 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
|| (!select && _selection.AnchorPoint != point)
|| _selection.InsertionPointAffinity != insertionPointAffinity)
{
- _selection = new Selection(point, select ? _selection.AnchorPoint : point, point, insertionPointAffinity);
+ var newSelection = new Selection(point, select ? _selection.AnchorPoint : point, point, insertionPointAffinity);
+
+ // Using the ternary here to shortcut out if the snapshots are the same. There's a similar check in the
+ // MapSelectionToCurrentSnapshot method to avoid doing unneeded work, but even spinning up the method call can be expensive.
+ _selection = (newSelection.InsertionPoint.Position.Snapshot == this.CurrentSnapshot)
+ ? newSelection
+ : MapSelectionToCurrentSnapshot(newSelection);
+
_broker.QueueCaretUpdatedEvent(this);
}
}
+ private bool LogException(Exception ex)
+ {
+ _broker.Factory.GuardedOperations.HandleException(this, ex);
+ return true;
+ }
+
+ private Selection MapSelectionToCurrentSnapshot(Selection newSelection)
+ {
+ if (newSelection.InsertionPoint.Position.Snapshot != this.CurrentSnapshot)
+ {
+ try
+ {
+ throw new InvalidOperationException("Selection does not match the current snapshot.");
+ }
+ catch (InvalidOperationException ex) when (LogException(ex))
+ {
+ // This will catch every time, since LogException always returns true.
+
+ // We really should throw here every time, but to limit the number of crashes we see from this immediately
+ // we are doing a best effort here, and only throwing when we can't map forward. Additionally, we're logging
+ // faults with guarded operations in order to identify bad actors so we can fix them before converting this
+ // to the 'throw always' route.
+
+ // This can still throw if mapping doesn't happen, but it should be significantly less often than currently.
+ newSelection = newSelection.MapToSnapshot(this.CurrentSnapshot, _broker.TextView);
+ }
+ }
+
+ return newSelection;
+ }
+
public void MoveTo(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint, VirtualSnapshotPoint insertionPoint, PositionAffinity insertionPointAffinity)
{
+ // See other overload of MoveTo for interesting comments.
this.CheckIsValid();
if (_selection.AnchorPoint != anchorPoint
@@ -368,7 +487,10 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
|| _selection.InsertionPoint != insertionPoint
|| _selection.InsertionPointAffinity != insertionPointAffinity)
{
- _selection = new Selection(insertionPoint, anchorPoint, activePoint, insertionPointAffinity);
+ var newSelection = new Selection(insertionPoint, anchorPoint, activePoint, insertionPointAffinity);
+ _selection = (newSelection.InsertionPoint.Position.Snapshot == this.CurrentSnapshot)
+ ? newSelection
+ : MapSelectionToCurrentSnapshot(newSelection);
_broker.QueueCaretUpdatedEvent(this);
}
}
@@ -469,7 +591,6 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
-
#region Predefined Caret Manipulations
public void ClearSelection()
{
@@ -695,6 +816,13 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
private void SelectCurrentWord()
{
var extent = _broker.TextStructureNavigator.GetExtentOfWord(this.Selection.InsertionPoint.Position);
+
+ // Select word left of caret if the token to the right is just whitespace.
+ if (!extent.IsSignificant && (extent.Span.Start.Position > 0))
+ {
+ extent = _broker.TextStructureNavigator.GetExtentOfWord(this.Selection.InsertionPoint.Position - 1);
+ }
+
var anchor = new VirtualSnapshotPoint(extent.Span.Start);
var active = new VirtualSnapshotPoint(extent.Span.End);
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs b/src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs
index 1a14405..51f2058 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs
@@ -69,25 +69,84 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
- public override ITextViewLine ContainingTextViewLine
+ public override bool TryGetContainingTextViewLine(out ITextViewLine line)
{
- get
+ line = null;
+ if (_broker.TextView.InLayout || _broker.TextView.IsClosed)
+ {
+ return false;
+ }
+
+ // There are cases where people implement ITextView without ITextView2, so I'm doing a best effort here
+ // checking InLayout and IsClosed manually rather than depending on ITextView2.TryGetTextViewLineContainingBufferPosition below.
+ // The try..catch is then just paranoia to avoid throwing from a TryGet* method at all costs.
+ try
{
var bufferPosition = _transformer.Selection.InsertionPoint.Position;
+
+ // Problematic, though it may be, some callers like to check this during a layout, which means our
+ // snapshot isn't always reliable. If this method comes up in a crash, look through the callstack for someone dispatching
+ // a call without looking to see if the view is in the layout.
+
ITextViewLine textLine = _broker.TextView.GetTextViewLineContainingBufferPosition(bufferPosition);
- if ((_transformer.Selection.InsertionPointAffinity == PositionAffinity.Predecessor) && (textLine.Start == bufferPosition) &&
+ line = _broker.TextView.GetTextViewLineContainingBufferPosition(bufferPosition);
+
+ if ((_transformer.Selection.InsertionPointAffinity == PositionAffinity.Predecessor) && (line.Start == bufferPosition) &&
(_broker.TextView.TextSnapshot.GetLineFromPosition(bufferPosition).Start != bufferPosition))
{
//The desired location has precedessor affinity at the start of a word wrapped line, so we
//really want the line before this one.
- textLine = _broker.TextView.GetTextViewLineContainingBufferPosition(bufferPosition - 1);
+ line = _broker.TextView.GetTextViewLineContainingBufferPosition(bufferPosition - 1);
}
- return textLine;
+ return line != null;
+ }
+ catch (Exception ex)
+ {
+ _factory.GuardedOperations.HandleException(this, ex);
+ line = null;
+ return false;
+ }
+ }
+
+ public override ITextViewLine ContainingTextViewLine
+ {
+ get
+ {
+ // Problematic, though it may be, some callers like to check this during a layout, which means our
+ // snapshot isn't always reliable. If this method comes up in a crash, look through the callstack for someone dispatching
+ // a call without looking to see if the view is in the layout.
+ //
+ // The property to check is ITextView.InLayout
+
+ if (TryGetContainingTextViewLine(out var line))
+ {
+ return line;
+ }
+ else
+ {
+ try
+ {
+ throw new InvalidOperationException("Unable to get TextViewLine containing insertion point.");
+ }
+ catch (InvalidOperationException ex) when (LogException(ex))
+ {
+ // This catch block will never be reached because LogException always returns false.
+ return null;
+ }
+ }
}
}
+ private bool LogException(Exception ex)
+ {
+ // Ok, this is weird. What we are doing here is using guarded operations to log errors to ActivityLogs and Telemetry,
+ // but we really have to throw here because by the time you get to this state there's no graceful recovery.
+ _factory.GuardedOperations.HandleException(this, ex);
+ return false;
+ }
+
public double CaretWidth
{
get
diff --git a/src/Text/Util/TextDataUtil/GuardedOperations.cs b/src/Text/Util/TextDataUtil/GuardedOperations.cs
index add89d2..98a6d40 100644
--- a/src/Text/Util/TextDataUtil/GuardedOperations.cs
+++ b/src/Text/Util/TextDataUtil/GuardedOperations.cs
@@ -747,16 +747,32 @@ namespace Microsoft.VisualStudio.Text.Utilities
}).Task;
}
- static bool _ignoreFailures = false;
+ internal static bool IgnoreFailures = false;
+ internal static bool BreakOnFailures = true;
+
+ public bool TryCastToType<TArgs>(object toCast, out TArgs casted)
+ {
+ try
+ {
+ casted = (TArgs)toCast;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ HandleException(this, ex);
+ casted = default(TArgs);
+ return false;
+ }
+ }
[Conditional("DEBUG")]
private static void Fail(string message)
{
- if (!_ignoreFailures)
+ if (!IgnoreFailures)
{
- if (Debugger.IsAttached)
+ if (BreakOnFailures && Debugger.IsAttached)
Debugger.Break();
- Debug.Fail(message);
+ Debug.Fail(message);
}
}
}
diff --git a/src/Text/Util/TextLogicUtil/SideBySideMap.cs b/src/Text/Util/TextLogicUtil/SideBySideMap.cs
new file mode 100644
index 0000000..2ec0faa
--- /dev/null
+++ b/src/Text/Util/TextLogicUtil/SideBySideMap.cs
@@ -0,0 +1,136 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Differencing
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ public class SideBySideMap
+ {
+ public ISnapshotDifference Difference { get; }
+
+ public int SideBySideLength { get; }
+
+ private IList<int> _sideBySideEndAtDiff;
+
+ /// <summary>
+ /// Create a Side-by-side map for the given snapshot difference. Note that only side-by-side line maps are supported
+ /// (character maps -- needed for word wrap -- will require a different approach since even "matches" could have different
+ /// lengths if ignore white space is turned on).
+ /// </summary>
+ public SideBySideMap(ISnapshotDifference difference)
+ {
+ Requires.NotNull(difference, nameof(difference));
+ this.Difference = difference;
+
+ // Calculate the effective document "length" (where the length of each diff block
+ // is the maximum of the right & left diffs) and store it for each diff.
+ //
+ // This code assumes word wrap is off. If it is on, we need to use the characters in each block.
+ _sideBySideEndAtDiff = new List<int>(difference.LineDifferences.Differences.Count);
+
+ var leftEnd = 0;
+ var rightEnd = 0;
+ var sideBySideEnd = 0;
+ for (int i = 0; (i < difference.LineDifferences.Differences.Count); ++i)
+ {
+ var diff = difference.LineDifferences.Differences[i];
+
+ int newLeftEnd = diff.Left.End;
+ int newRightEnd = diff.Right.End;
+
+ var leftDelta = newLeftEnd - leftEnd;
+ var rightDelta = newRightEnd - rightEnd;
+
+ sideBySideEnd += Math.Max(leftDelta, rightDelta);
+ _sideBySideEndAtDiff.Add(sideBySideEnd);
+
+ leftEnd = newLeftEnd;
+ rightEnd = newRightEnd;
+ }
+
+ // Since we're in a match, (current.RightBufferSnapshot.LineCount - rightEnd) == current.LeftBufferSnapshot.LineCount - leftEnd
+ this.SideBySideLength = sideBySideEnd + difference.RightBufferSnapshot.LineCount - rightEnd;
+ }
+
+ /// <summary>
+ /// Return the buffer position that corresponds to the specified coordinate (which will be either a line number or a character position). The returned position may be
+ /// on either the left or right difference snapshots.
+ /// </summary>
+ public SnapshotPoint BufferPositionFromSideBySideCoordinate(int coordinate)
+ {
+ //Convert the coordinate to a valid line number (0 ... line count - 1).
+ coordinate = Math.Min(Math.Max(0, coordinate), (this.SideBySideLength - 1));
+
+ ListUtilities.BinarySearch(_sideBySideEndAtDiff, (s) => (s - coordinate - 1), out int index);
+
+ ITextSnapshotLine line;
+ if (index >= this.Difference.LineDifferences.Differences.Count)
+ {
+ // We're in a match that follows the last difference (assuming there are any differences).
+ // Count lines backwards from the end of the right buffer.
+ var delta = this.SideBySideLength - coordinate;
+ line = this.Difference.RightBufferSnapshot.GetLineFromLineNumber(this.Difference.RightBufferSnapshot.LineCount - delta);
+ }
+ else
+ {
+ // We either in a difference or in the match that preceeds a difference.
+ // In either case, the sideBySideEnd corresponds to the start of the match.
+ int sideBySideEnd = (index > 0) ? _sideBySideEndAtDiff[index - 1] : 0;
+
+ int delta = coordinate - sideBySideEnd;
+ Span left;
+ Span right;
+ var difference = this.Difference.LineDifferences.Differences[index];
+ left = difference.Left;
+ right = difference.Right;
+ if (difference.Before != null)
+ {
+ left = Span.FromBounds(difference.Before.Left.Start, difference.Left.End);
+ right = Span.FromBounds(difference.Before.Right.Start, difference.Right.End);
+ }
+
+ if (delta < right.Length)
+ {
+ line = this.Difference.RightBufferSnapshot.GetLineFromLineNumber(right.Start + delta);
+ }
+ else
+ {
+ Debug.Assert(delta < left.Length);
+ line = this.Difference.LeftBufferSnapshot.GetLineFromLineNumber(left.Start + delta);
+ }
+ }
+
+ return line.Start;
+ }
+
+ /// <summary>
+ /// Return the coordinate of the given buffer position (which can be on the left or right buffers).
+ /// </summary>
+ public int SideBySideCoordinateFromBufferPosition(SnapshotPoint position)
+ {
+ position = this.Difference.TranslateToSnapshot(position);
+ int lineNumber = position.GetContainingLine().LineNumber;
+ int index = this.Difference.FindMatchOrDifference(position, out Match match, out Difference difference);
+ if (index == 0)
+ {
+ // Either in a leading match or the 1st difference. In either case, the real line number is the side-by-side line number.
+ return lineNumber;
+ }
+
+ difference = this.Difference.LineDifferences.Differences[index - 1];
+ if (position.Snapshot == this.Difference.LeftBufferSnapshot)
+ {
+ return _sideBySideEndAtDiff[index - 1] + (lineNumber - difference.Left.End);
+ }
+ else
+ {
+ return _sideBySideEndAtDiff[index - 1] + (lineNumber - difference.Right.End);
+ }
+ }
+ }
+}
diff --git a/src/Text/Util/TextUIUtil/ExtensionMethods.cs b/src/Text/Util/TextUIUtil/ExtensionMethods.cs
index c791f15..4d93bce 100644
--- a/src/Text/Util/TextUIUtil/ExtensionMethods.cs
+++ b/src/Text/Util/TextUIUtil/ExtensionMethods.cs
@@ -30,8 +30,22 @@ namespace Microsoft.VisualStudio.Text.MultiSelection
var newInsertion = view.NormalizePoint(region.InsertionPoint.TranslateTo(snapshot));
var newActive = view.NormalizePoint(region.ActivePoint.TranslateTo(snapshot));
var newAnchor = view.NormalizePoint(region.AnchorPoint.TranslateTo(snapshot));
+ PositionAffinity positionAffinity;
- return new Selection(newInsertion, newAnchor, newActive, region.InsertionPointAffinity);
+ if (region.Extent.Length == 0)
+ {
+ // Selection is just a caret, respect the caret's prefered affinity.
+ positionAffinity = region.InsertionPointAffinity;
+ }
+ else
+ {
+ // Selection is non-zero length, adjust affinity so that it is always toward the body of the selection.
+ // This attempts to ensure that the caret is always on the same line as the body of the selection in
+ // word wrap scenarios.
+ positionAffinity = newAnchor < newActive ? PositionAffinity.Predecessor : PositionAffinity.Successor;
+ }
+
+ return new Selection(newInsertion, newAnchor, newActive, positionAffinity);
}
/// <summary>