diff options
author | Kirill Osenkov <github@osenkov.com> | 2018-12-06 02:09:58 +0300 |
---|---|---|
committer | Kirill Osenkov <github@osenkov.com> | 2018-12-06 02:09:58 +0300 |
commit | 30e88b1f2ce3ee9897e1f3b87c80c64292c27236 (patch) | |
tree | 45ec1d0b3e72b459533fdff426f7cfc958ea87f7 /src | |
parent | 501ae272174261c9d8a60159e0a162a0af4f8922 (diff) |
Update to VS-Platform 16.0.142-g25b7188c54.
25b7188c54f0cdf6a5be87eeb38e3c6046d5788e
Diffstat (limited to 'src')
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 '{0}' 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> |