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

github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src/Text
diff options
context:
space:
mode:
authorKirill Osenkov <github@osenkov.com>2018-12-06 02:09:58 +0300
committerKirill Osenkov <github@osenkov.com>2018-12-06 02:09:58 +0300
commit30e88b1f2ce3ee9897e1f3b87c80c64292c27236 (patch)
tree45ec1d0b3e72b459533fdff426f7cfc958ea87f7 /src/Text
parent501ae272174261c9d8a60159e0a162a0af4f8922 (diff)
Update to VS-Platform 16.0.142-g25b7188c54.
25b7188c54f0cdf6a5be87eeb38e3c6046d5788e
Diffstat (limited to 'src/Text')
-rw-r--r--src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs40
-rw-r--r--src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs11
-rw-r--r--src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs20
-rw-r--r--src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs20
-rw-r--r--src/Text/Def/Internal/TextLogic/ParentAttribute.cs20
-rw-r--r--src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs22
-rw-r--r--src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs142
-rw-r--r--src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs7
-rw-r--r--src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs16
-rw-r--r--src/Text/Def/TextUI/Editor/ITextView2.cs51
-rw-r--r--src/Text/Def/TextUI/Editor/TextViewExtensions.cs71
-rw-r--r--src/Text/Def/TextUI/EditorOptions/ViewOptions.cs28
-rw-r--r--src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs8
-rw-r--r--src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs100
-rw-r--r--src/Text/Def/TextUI/Utilities/IStatusBarService.cs22
-rw-r--r--src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs22
-rw-r--r--src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs44
-rw-r--r--src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs64
-rw-r--r--src/Text/Impl/Commanding/CommandingStrings.Designer.cs11
-rw-r--r--src/Text/Impl/Commanding/CommandingStrings.resx3
-rw-r--r--src/Text/Impl/Commanding/EditorCommandHandlerService.cs211
-rw-r--r--src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs45
-rw-r--r--src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs55
-rw-r--r--src/Text/Impl/EditorOperations/EditorOperations.cs653
-rw-r--r--src/Text/Impl/EditorOptions/EditorOptions.cs13
-rw-r--r--src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs34
-rw-r--r--src/Text/Impl/Outlining/OutliningManager.cs45
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs78
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs71
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs256
-rw-r--r--src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs69
-rw-r--r--src/Text/Util/TextDataUtil/GuardedOperations.cs24
-rw-r--r--src/Text/Util/TextLogicUtil/SideBySideMap.cs136
-rw-r--r--src/Text/Util/TextUIUtil/ExtensionMethods.cs16
34 files changed, 2083 insertions, 345 deletions
diff --git a/src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs b/src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs
new file mode 100644
index 0000000..ddf8714
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs
@@ -0,0 +1,40 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+// This file contain internal APIs that are subject to change without notice.
+// Use at your own risk.
+//
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.Editor
+{
+ /// <summary>
+ /// Support the scenario where we want to create the editor options and then create the corresponding view (so that the editor
+ /// options can be seeded with options the view will want).
+ /// </summary>
+ public interface IEditorOptionsFactoryService2 : IEditorOptionsFactoryService
+ {
+ /// <summary>
+ /// Create a new <see cref="IEditorOptions"/> that is not bound to a particular scope.
+ /// </summary>
+ /// <param name="allowLateBinding">If true, this option can be bound to a scope after it has been created using <see cref="TryBindToScope(IEditorOptions, IPropertyOwner)"/>.</param>
+ /// <returns></returns>
+ IEditorOptions CreateOptions(bool allowLateBinding);
+
+
+ /// <summary>
+ /// Binds <paramref name="option"/> to the specified scope if the scope does not have pre-existing <see cref="IEditorOptions"/> and <paramref name="option"/> was
+ /// created using <see cref="CreateOptions(bool)"/> with the late binding allowed.
+ /// </summary>
+ /// <returns>true if <paramref name="option"/> was bound to <paramref name="scope"/>.</returns>
+ bool TryBindToScope(IEditorOptions option, IPropertyOwner scope);
+
+ /// <summary>
+ /// Get the option definition associated with <paramref name="optionId"/>.
+ /// </summary>
+ /// <param name="optionId"></param>
+ /// <returns></returns>
+ EditorOptionDefinition GetOptionDefinition(string optionId);
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs
index dad1d94..936da15 100644
--- a/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs
+++ b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs
@@ -61,12 +61,15 @@ namespace Microsoft.VisualStudio.Text.Utilities
/// Changing this will force the event to send to Watson. Be careful because it can have big perf impact.
/// If unchanged, it will be set according to the default sample rate.
/// </param>
+ /// <param name="correlations">TelemetryEventCorrelations which help correlate this fault to the scope it was executing within</param>
void PostFault(
string eventName,
string description,
Exception exceptionObject,
- string additionalErrorInfo,
- bool? isIncludedInWatsonSample);
+ string additionalErrorInfo = null,
+ bool? isIncludedInWatsonSample = null,
+ object[] correlations = null
+ );
/// <summary>
/// Adjust the counter associated with <paramref name="key"/> and <paramref name="name"/> by <paramref name="delta"/>.
@@ -85,5 +88,9 @@ namespace Microsoft.VisualStudio.Text.Utilities
/// <para>The counters are cleared as a side-effect of this call.</para>
/// </remarks>
void PostCounters();
+
+ object CreateTelemetryOperationEventScope(string eventName, TelemetrySeverity severity, object[] correlations, IDictionary<string, object> startingProperties);
+ object GetCorrelationFromTelemetryScope(object telemetryScope);
+ void EndTelemetryScope(object telemetryScope, TelemetryResult result, string summary = null);
}
}
diff --git a/src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs b/src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs
new file mode 100644
index 0000000..11a0080
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs
@@ -0,0 +1,20 @@
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.OptionDescriptions
+{
+ /// <summary>
+ /// Attribute defining whether or not the description has an associated option.
+ /// </summary>
+ /// <remarks>
+ /// Defaults to true. Set to false for static elements in the options page.
+ /// </remarks>
+ public sealed class IsOptionAttribute : SingletonBaseMetadataAttribute
+ {
+ public IsOptionAttribute(bool isOption)
+ {
+ this.IsOption = isOption;
+ }
+
+ public bool IsOption { get; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs b/src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs
new file mode 100644
index 0000000..f681510
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs
@@ -0,0 +1,20 @@
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.OptionDescriptions
+{
+ /// <summary>
+ /// Attribute defining whether or not the option roams.
+ /// </summary>
+ /// <remarks>
+ /// If not provided, then the option is not considered a roaming attribute.
+ /// </remarks>
+ public sealed class IsRoamingAttribute : SingletonBaseMetadataAttribute
+ {
+ public IsRoamingAttribute(bool isRoaming = true)
+ {
+ this.IsRoaming = isRoaming;
+ }
+
+ public bool IsRoaming { get; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/ParentAttribute.cs b/src/Text/Def/Internal/TextLogic/ParentAttribute.cs
new file mode 100644
index 0000000..9951024
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/ParentAttribute.cs
@@ -0,0 +1,20 @@
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.Text.OptionDescriptions
+{
+ /// <summary>
+ /// Attribute indicating the parent option/page of the option.
+ /// </summary>
+ /// <remarks>
+ /// Options can be nested or attached directly to the their corresponding page.
+ /// </remarks>
+ public sealed class ParentAttribute : SingletonBaseMetadataAttribute
+ {
+ public ParentAttribute(string parent)
+ {
+ this.Parent = parent;
+ }
+
+ public string Parent { get; }
+ }
+}
diff --git a/src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs b/src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs
new file mode 100644
index 0000000..7bbf5e2
--- /dev/null
+++ b/src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs
@@ -0,0 +1,22 @@
+namespace Microsoft.VisualStudio.Text.Utilities
+{
+ //
+ // Summary:
+ // An enum to define the severity of the telemetry event. It is used for any data
+ // consumer who wants to categorize data based on severity.
+ public enum TelemetrySeverity : int
+ {
+ //
+ // Summary:
+ // indicates telemetry event with verbose information.
+ Low = -10,
+ //
+ // Summary:
+ // indicates a regular telemetry event.
+ Normal = 0,
+ //
+ // Summary:
+ // indicates telemetry event with high value or require attention (e.g., fault).
+ High = 10
+ }
+}
diff --git a/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs b/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs
index d1e7af1..996e758 100644
--- a/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs
+++ b/src/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs
@@ -4,6 +4,7 @@
//
using System;
using System.ComponentModel.Composition;
+using System.Threading;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods
@@ -204,6 +205,50 @@ namespace Microsoft.VisualStudio.Text.Editor
public static readonly EditorOptionKey<string> TooltipAppearanceCategoryOptionId = new EditorOptionKey<string>(TooltipAppearanceCategoryOptionName);
public const string TooltipAppearanceCategoryOptionName = "TooltipAppearanceCategory";
+ /// <summary>
+ /// The default option that determines whether files, when opened, attempt to detect for a utf-8 encoding.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> AutoDetectUtf8Id = new EditorOptionKey<bool>(AutoDetectUtf8Name);
+ public const string AutoDetectUtf8Name = "AutoDetectUtf8";
+
+ /// <summary>
+ /// The default option that determines whether matching delimiters should be highlighted.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> AutomaticDelimiterHighlightingId = new EditorOptionKey<bool>(AutomaticDelimiterHighlightingName);
+ public const string AutomaticDelimiterHighlightingName = "AutomaticDelimiterHighlighting";
+
+ /// <summary>
+ /// The default option that determines whether files should follow project coding conventions.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> FollowCodingConventionsId = new EditorOptionKey<bool>(FollowCodingConventionsName);
+ public const string FollowCodingConventionsName = "FollowCodingConventions";
+
+ /// <summary>
+ /// The default option that determines the editor emulation mode.
+ /// </summary>
+ public static readonly EditorOptionKey<int> EditorEmulationModeId = new EditorOptionKey<int>(EditorEmulationModeName);
+ public const string EditorEmulationModeName = "EditorEmulationMode";
+
+ /// <summary>
+ /// The option definition that determines maximum allowed typing latency value in milliseconds. Its value comes either
+ /// from remote settings or from <see cref="UserCustomMaximumTypingLatencyOption"/> if user specifies it in
+ /// Tools/Options/Text Editor/Advanced page.
+ /// </summary>
+ internal static readonly EditorOptionKey<int> MaximumTypingLatencyOptionId = new EditorOptionKey<int>(MaximumTypingLatencyOptionName);
+ internal const string MaximumTypingLatencyOptionName = "MaximumTypingLatency";
+
+ /// <summary>
+ /// The option definition that determines user custom maximum allowed typing latency value in milliseconds. If user
+ /// specifies it on Tools/Options/Text Editor/Advanced page, it becomes a source for the <see cref="MaximumTypingLatency"/> option.
+ /// </summary>
+ internal static readonly EditorOptionKey<int> UserCustomMaximumTypingLatencyOptionId = new EditorOptionKey<int>(UserCustomMaximumTypingLatencyOptionName);
+ internal const string UserCustomMaximumTypingLatencyOptionName = "UserCustomMaximumTypingLatency";
+
+ /// <summary>
+ /// The option definition that determines whether to enable typing latency guarding.
+ /// </summary>
+ internal static readonly EditorOptionKey<bool> EnableTypingLatencyGuardOptionId = new EditorOptionKey<bool>(EnableTypingLatencyGuardOptionName);
+ internal const string EnableTypingLatencyGuardOptionName = "EnableTypingLatencyGuard";
#endregion
}
@@ -408,5 +453,102 @@ namespace Microsoft.VisualStudio.Text.Editor
public override EditorOptionKey<string> Key { get { return DefaultOptions.TooltipAppearanceCategoryOptionId; } }
}
+ /// <summary>
+ /// The option definition that determines whether files, when opened, attempt to detect for a utf-8 encoding.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.AutoDetectUtf8Name)]
+ public sealed class AutoDetectUtf8Option : EditorOptionDefinition<bool>
+ {
+ public override bool Default { get => true; }
+
+ public override EditorOptionKey<bool> Key { get { return DefaultOptions.AutoDetectUtf8Id; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines whether matching delimiters should be highlighted.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.AutomaticDelimiterHighlightingName)]
+ public sealed class AutomaticDelimiterHighlightingOption : EditorOptionDefinition<bool>
+ {
+ public override bool Default { get => true; }
+
+ public override EditorOptionKey<bool> Key { get { return DefaultOptions.AutomaticDelimiterHighlightingId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines whether files should follow project coding conventions.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.FollowCodingConventionsName)]
+ public sealed class FollowCodingConventionsOption : EditorOptionDefinition<bool>
+ {
+ public override bool Default { get => true; }
+
+ public override EditorOptionKey<bool> Key { get { return DefaultOptions.FollowCodingConventionsId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines the editor emulation mode.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.EditorEmulationModeName)]
+ public sealed class EditorEmulationModeOption : EditorOptionDefinition<int>
+ {
+ public override int Default { get => 0; }
+
+ public override EditorOptionKey<int> Key { get { return DefaultOptions.EditorEmulationModeId; } }
+ }
+
+ /// <summary>
+ ///The option definition that determines whether to enable typing latency guarding.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.EnableTypingLatencyGuardOptionName)]
+ internal sealed class EnableTypingLatencyGuard : EditorOptionDefinition<bool>
+ {
+ /// <summary>
+ /// Gets the default value (true).
+ /// </summary>
+ public override bool Default { get => true; }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<bool> Key { get { return DefaultOptions.EnableTypingLatencyGuardOptionId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines maximum allowed typing latency value in milliseconds. Its value comes either
+ /// from remote settings or from <see cref="UserCustomMaximumTypingLatencyOption"/> if user specifies it in
+ /// Tools/Options/Text Editor/Advanced page.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.MaximumTypingLatencyOptionName)]
+ internal sealed class MaximumTypingLatency : EditorOptionDefinition<int>
+ {
+ /// <summary>
+ /// Gets the default value (infinite).
+ /// </summary>
+ public override int Default { get => Timeout.Infinite; }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<int> Key { get { return DefaultOptions.MaximumTypingLatencyOptionId; } }
+ }
+
+ /// <summary>
+ /// The option definition that determines user custom maximum allowed typing latency value in milliseconds. If user
+ /// specifies it on Tools/Options/Text Editor/Advanced page, it becomes a source for the <see cref="MaximumTypingLatency"/> option.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultOptions.UserCustomMaximumTypingLatencyOptionName)]
+ internal sealed class UserCustomMaximumTypingLatencyOption : EditorOptionDefinition<int>
+ {
+ public override int Default { get { return Timeout.Infinite; } }
+ public override EditorOptionKey<int> Key { get { return DefaultOptions.UserCustomMaximumTypingLatencyOptionId; } }
+ }
#endregion
}
diff --git a/src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs b/src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs
index 290f256..d4b0ba3 100644
--- a/src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs
+++ b/src/Text/Def/TextUI/DifferenceViewer/DifferenceViewerOptions.cs
@@ -34,6 +34,13 @@ namespace Microsoft.VisualStudio.Text.Differencing
/// <remarks>This option is ignored in the other view modes.</remarks>
public static readonly EditorOptionKey<bool> SynchronizeSideBySideViewsId = new EditorOptionKey<bool>(DifferenceViewerOptions.SynchronizeSideBySideViewsName);
public const string SynchronizeSideBySideViewsName = "Diff/View/SynchronizeSideBySideViews";
+
+
+ /// <summary>
+ /// If <c>true</c>, show the difference overview margin.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> ShowDiffOverviewMarginId = new EditorOptionKey<bool>(DifferenceViewerOptions.ShowDiffOverviewMarginName);
+ public const string ShowDiffOverviewMarginName = "Diff/View/ShowDiffOverviewMargin";
}
/// <summary>
diff --git a/src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs b/src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs
new file mode 100644
index 0000000..3b9a4a8
--- /dev/null
+++ b/src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs
@@ -0,0 +1,16 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Differencing
+{
+ using System;
+
+ public interface IDifferenceViewer2 : IDifferenceViewer
+ {
+ /// <summary>
+ /// Raised when the difference viewer is fully initialized.
+ /// </summary>
+ event EventHandler Initialized;
+ }
+}
diff --git a/src/Text/Def/TextUI/Editor/ITextView2.cs b/src/Text/Def/TextUI/Editor/ITextView2.cs
index ed797ee..0469f75 100644
--- a/src/Text/Def/TextUI/Editor/ITextView2.cs
+++ b/src/Text/Def/TextUI/Editor/ITextView2.cs
@@ -4,6 +4,7 @@
//
using System;
+using Microsoft.VisualStudio.Text.Formatting;
namespace Microsoft.VisualStudio.Text.Editor
{
@@ -33,7 +34,6 @@ namespace Microsoft.VisualStudio.Text.Editor
get;
}
-
/// <summary>
/// Raised whenever the view's MaxTextRightCoordinate is changed.
/// </summary>
@@ -42,5 +42,54 @@ namespace Microsoft.VisualStudio.Text.Editor
/// (it will not be raised as a side-effect of a layout even if the layout does change the MaxTextRightCoordinate).
/// </remarks>
event EventHandler MaxTextRightCoordinateChanged;
+
+ /// <summary>
+ /// Adds an action to be performed after any layouts are complete. If there is not a layout in progress, the action will
+ /// be performed immediately. This must be called on the UI thread, and actions will be performed on the UI thread.
+ /// </summary>
+ /// <param name="action">The action to be performed.</param>
+ void QueuePostLayoutAction(Action action);
+
+ /// <summary>
+ /// Attempts to get a read-only list of the <see cref="ITextViewLine"/> objects rendered in this view.
+ /// </summary>
+ /// <remarks>
+ /// This list will be dense. That is, all characters between the first character of the first <see cref="ITextViewLine"/> through
+ /// the last character of the last <see cref="ITextViewLine"/> will be represented in one of the <see cref="ITextViewLine"/> objects,
+ /// except when the layout of the <see cref="ITextViewLine"/> objects is in progress.
+ /// <para>
+ /// <see cref="ITextViewLine"/> objects are disjoint. That is, a given character is part of only one <see cref="ITextViewLine"/>.
+ /// </para>
+ /// <para>
+ /// The <see cref="ITextViewLine"/> objects are sorted by the index of their first character.
+ /// </para>
+ /// <para>Some of the <see cref="ITextViewLine"/> objects may not be visible,
+ /// and all <see cref="ITextViewLine"/> objects will be disposed of when the view
+ /// recomputes its layout.</para>
+ /// <para>This list is occasionally not available due to layouts or other events, and callers should be prepared to handle
+ /// a failure.</para>
+ /// </remarks>
+ /// <param name="textViewLines">Returns out the <see cref="ITextViewLineCollection"/> requested.</param>
+ /// <returns>True if succeeded, false otherwise.</returns>
+ bool TryGetTextViewLines(out ITextViewLineCollection textViewLines);
+
+ /// <summary>
+ /// Attempts to get the <see cref="ITextViewLine"/> that contains the specified text buffer position.
+ /// </summary>
+ /// <param name="bufferPosition">
+ /// The text buffer position used to search for a text line.
+ /// </param>
+ /// <returns>
+ /// True if succeeded, false otherwise.
+ /// </returns>
+ /// <remarks>
+ /// <para>This method returns an <see cref="ITextViewLine"/> if it exists in the view.</para>
+ /// <para>If the line does not exist in the cache of formatted lines, it will be formatted and added to the cache.</para>
+ /// <para>The returned <see cref="ITextViewLine"/> could be invalidated by either a layout by the view or by subsequent calls to this method.</para>
+ /// <para>It is occasionally invalid to retrieve an <see cref="ITextViewLine"/> due to layouts or other events. Callers should be prepared to handle
+ /// a failure.</para>
+ /// </remarks>
+ /// <param name="textViewLine">Returns out the <see cref="ITextViewLine"/> requested.</param>
+ bool TryGetTextViewLineContainingBufferPosition(SnapshotPoint bufferPosition, out ITextViewLine textViewLine);
}
}
diff --git a/src/Text/Def/TextUI/Editor/TextViewExtensions.cs b/src/Text/Def/TextUI/Editor/TextViewExtensions.cs
index ae2cfab..18354f1 100644
--- a/src/Text/Def/TextUI/Editor/TextViewExtensions.cs
+++ b/src/Text/Def/TextUI/Editor/TextViewExtensions.cs
@@ -3,6 +3,8 @@
// Licensed under the MIT License. See License.txt in the project root for license information.
//
using System;
+using System.Diagnostics;
+using Microsoft.VisualStudio.Text.Formatting;
namespace Microsoft.VisualStudio.Text.Editor
{
@@ -84,7 +86,74 @@ namespace Microsoft.VisualStudio.Text.Editor
throw new ArgumentNullException(nameof(textView));
}
- return ((ITextView2)textView).MultiSelectionBroker;
+ if (textView is ITextView2 textView2)
+ {
+ return textView2.MultiSelectionBroker;
+ }
+
+ if (textView.Properties.TryGetProperty(typeof(IMultiSelectionBroker), out IMultiSelectionBroker broker))
+ {
+ return broker;
+ }
+
+ Debug.Fail("Failed to acquire IMultiSelectionBroker for a text view");
+
+ return null;
+ }
+
+ /// <summary>
+ /// See <see cref="ITextView2.QueuePostLayoutAction(Action)"/>.
+ /// </summary>
+ public static void QueuePostLayoutAction(this ITextView textView, Action action)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException(nameof(textView));
+ }
+
+ (textView as ITextView2)?.QueuePostLayoutAction(action);
+ }
+
+ /// <summary>
+ /// See <see cref="ITextView2.TryGetTextViewLines(out ITextViewLineCollection)"/>.
+ /// </summary>
+ public static bool TryGetTextViewLines(this ITextView textView, out ITextViewLineCollection textViewLines)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException(nameof(textView));
+ }
+
+ if (textView is ITextView2 textView2)
+ {
+ return textView2.TryGetTextViewLines(out textViewLines);
+ }
+ else
+ {
+ textViewLines = null;
+ return false;
+ }
+ }
+
+ /// <summary>
+ /// See <see cref="ITextView2.TryGetTextViewLineContainingBufferPosition(SnapshotPoint, out Formatting.ITextViewLine)"/>.
+ /// </summary>
+ public static bool TryGetTextViewLineContainingBufferPosition(this ITextView textView, SnapshotPoint bufferPosition, out ITextViewLine textViewLine)
+ {
+ if (textView == null)
+ {
+ throw new ArgumentNullException(nameof(textView));
+ }
+
+ if (textView is ITextView2 textView2)
+ {
+ return textView2.TryGetTextViewLineContainingBufferPosition(bufferPosition, out textViewLine);
+ }
+ else
+ {
+ textViewLine = null;
+ return false;
+ }
}
}
}
diff --git a/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs b/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs
index b35cb6f..fc50df7 100644
--- a/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs
+++ b/src/Text/Def/TextUI/EditorOptions/ViewOptions.cs
@@ -536,6 +536,12 @@ namespace Microsoft.VisualStudio.Text.Editor
public const string ErrorMarginWidthOptionName = "OverviewMargin/ErrorMarginWidth";
public readonly static EditorOptionKey<double> ErrorMarginWidthOptionId = new EditorOptionKey<double>(ErrorMarginWidthOptionName);
+ /// <summary>
+ /// Determines whether to have a file health indicator.
+ /// </summary>
+ public static readonly EditorOptionKey<bool> EnableFileHealthIndicatorOptionId = new EditorOptionKey<bool>(EnableFileHealthIndicatorOptionName);
+ public const string EnableFileHealthIndicatorOptionName = "TextViewHost/FileHealthIndicator";
+
#endregion
}
@@ -658,7 +664,7 @@ namespace Microsoft.VisualStudio.Text.Editor
/// <summary>
/// Gets the default value, which is <c>WordWrapStyles.None</c>.
/// </summary>
- public override WordWrapStyles Default { get { return WordWrapStyles.None; } }
+ public override WordWrapStyles Default { get { return WordWrapStyles.AutoIndent; } }
/// <summary>
/// Gets the default text view host value.
@@ -893,7 +899,7 @@ namespace Microsoft.VisualStudio.Text.Editor
/// <summary>
/// Gets the default value, which is <c>false</c>.
/// </summary>
- public override bool Default { get { return false; } }
+ public override bool Default { get { return true; } }
/// <summary>
/// Gets the default text view host value.
@@ -1002,4 +1008,22 @@ namespace Microsoft.VisualStudio.Text.Editor
/// </summary>
public override EditorOptionKey<double> Key => DefaultTextViewOptions.CaretWidthId;
}
+
+ /// <summary>
+ /// Defines the option to enable the File Health Indicator.
+ /// </summary>
+ [Export(typeof(EditorOptionDefinition))]
+ [Name(DefaultTextViewHostOptions.EnableFileHealthIndicatorOptionName)]
+ public sealed class FileHealthIndicatorEnabled : ViewOptionDefinition<bool>
+ {
+ /// <summary>
+ /// Gets the default value, which is <c>true</c>.
+ /// </summary>
+ public override bool Default { get { return true; } }
+
+ /// <summary>
+ /// Gets the default text view host value.
+ /// </summary>
+ public override EditorOptionKey<bool> Key { get { return DefaultTextViewHostOptions.EnableFileHealthIndicatorOptionId; } }
+ }
}
diff --git a/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs b/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs
index 351d733..b77878e 100644
--- a/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs
+++ b/src/Text/Def/TextUI/MultiCaret/AbstractSelectionPresentationProperties.cs
@@ -45,5 +45,13 @@ namespace Microsoft.VisualStudio.Text
/// Gets the <see cref="ITextViewLine"/> that contains the <see cref="Selection.InsertionPoint"/>.
/// </summary>
public virtual ITextViewLine ContainingTextViewLine { get; }
+
+ /// <summary>
+ /// Tries to get the <see cref="ITextViewLine"/> that contains the <see cref="Selection.InsertionPoint"/>.
+ /// This can fail if the call happens during a view layout or after the view is closed.
+ /// </summary>
+ /// <param name="line">Returns out the requested line if available, or null otherwise.</param>
+ /// <returns>True if successful, false otherwise.</returns>
+ public abstract bool TryGetContainingTextViewLine(out ITextViewLine line);
}
}
diff --git a/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs b/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs
index 05a522b..85f4f0f 100644
--- a/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs
+++ b/src/Text/Def/TextUI/Utilities/AbstractUIThreadOperationContext.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using System.Threading;
@@ -12,7 +13,7 @@ namespace Microsoft.VisualStudio.Utilities
public abstract class AbstractUIThreadOperationContext : IUIThreadOperationContext
#pragma warning restore CA1063 // Implement IDisposable Correctly
{
- private List<IUIThreadOperationScope> _scopes;
+ private ImmutableList<IUIThreadOperationScope> _scopes;
private bool _allowCancellation;
private PropertyCollection _properties;
private readonly string _defaultDescription;
@@ -30,6 +31,7 @@ namespace Microsoft.VisualStudio.Utilities
{
_defaultDescription = defaultDescription ?? throw new ArgumentNullException(nameof(defaultDescription));
_allowCancellation = allowCancellation;
+ _scopes = ImmutableList<IUIThreadOperationScope>.Empty;
}
/// <summary>
@@ -56,12 +58,14 @@ namespace Microsoft.VisualStudio.Utilities
return false;
}
- if (_scopes == null || _scopes.Count == 0)
+ ImmutableList<IUIThreadOperationScope> scopes = _scopes;
+
+ if (scopes == null || scopes.Count == 0)
{
return _allowCancellation;
}
- return _scopes.All((s) => s.AllowCancellation);
+ return scopes.All((s) => s.AllowCancellation);
}
}
@@ -78,31 +82,42 @@ namespace Microsoft.VisualStudio.Utilities
return _defaultDescription;
}
+ ImmutableList<IUIThreadOperationScope> scopes = _scopes;
+
// Most common case
- if (_scopes.Count == 1)
+ if (scopes.Count == 1)
{
- return _scopes[0].Description;
+ return scopes[0].Description;
}
// Combine descriptions of all current scopes
- return string.Join(Environment.NewLine, _scopes.Select((s) => s.Description));
+ return string.Join(Environment.NewLine, scopes.Select((s) => s.Description));
}
}
protected int CompletedItems => _completedItems;
protected int TotalItems => _totalItems;
- private IList<IUIThreadOperationScope> LazyScopes => _scopes ?? (_scopes = new List<IUIThreadOperationScope>());
-
/// <summary>
/// Gets current list of <see cref="IUIThreadOperationScope"/>s in this context.
/// </summary>
- public virtual IEnumerable<IUIThreadOperationScope> Scopes => this.LazyScopes;
+ public virtual IEnumerable<IUIThreadOperationScope> Scopes => _scopes;
/// <summary>
/// A collection of properties.
/// </summary>
- public virtual PropertyCollection Properties => _properties ?? (_properties = new PropertyCollection());
+ public virtual PropertyCollection Properties
+ {
+ get
+ {
+ if (_properties == null)
+ {
+ Interlocked.CompareExchange(ref _properties, new PropertyCollection(), null);
+ }
+
+ return _properties;
+ }
+ }
/// <summary>
/// Adds an UI thread operation scope with its own cancellability, description and progress tracker.
@@ -111,7 +126,20 @@ namespace Microsoft.VisualStudio.Utilities
public virtual IUIThreadOperationScope AddScope(bool allowCancellation, string description)
{
var scope = new UIThreadOperationScope(allowCancellation, description, this);
- this.LazyScopes.Add(scope);
+
+ while (true)
+ {
+ ImmutableList<IUIThreadOperationScope> oldScopes = _scopes;
+ ImmutableList<IUIThreadOperationScope> newScopes = oldScopes == null ? ImmutableList.Create<IUIThreadOperationScope>(scope) : oldScopes.Add(scope);
+
+ var currentScopes = Interlocked.CompareExchange(ref _scopes, newScopes, oldScopes);
+ if (currentScopes == oldScopes)
+ {
+ // No other thread preempted us, new scopes set successfully
+ break;
+ }
+ }
+
this.OnScopesChanged();
return scope;
}
@@ -120,7 +148,14 @@ namespace Microsoft.VisualStudio.Utilities
{
int completed = 0;
int total = 0;
- foreach (UIThreadOperationScope scope in this.LazyScopes)
+
+ ImmutableList<IUIThreadOperationScope> scopes = _scopes;
+ if (scopes == null)
+ {
+ return;
+ }
+
+ foreach (UIThreadOperationScope scope in scopes)
{
completed += scope.CompletedItems;
total += scope.TotalItems;
@@ -159,16 +194,37 @@ namespace Microsoft.VisualStudio.Utilities
protected virtual void OnScopeDisposed(IUIThreadOperationScope scope)
{
+ if (scope == null)
+ {
+ return;
+ }
+
_allowCancellation &= scope.AllowCancellation;
- _scopes.Remove(scope);
+
+ if (_scopes == null)
+ {
+ return;
+ }
+
+ while (true) {
+ ImmutableList<IUIThreadOperationScope> oldScopes = _scopes;
+ ImmutableList<IUIThreadOperationScope> newScopes = oldScopes.Remove(scope);
+
+ var currentScopes = Interlocked.CompareExchange(ref _scopes, newScopes, oldScopes);
+ if (currentScopes == oldScopes)
+ {
+ // No other thread preempted us, new scopes set successfully
+ break;
+ }
+ }
+
OnScopesChanged();
}
- private class UIThreadOperationScope : IUIThreadOperationScope
+ private class UIThreadOperationScope : IUIThreadOperationScope, IProgress<ProgressInfo>
{
private bool _allowCancellation;
private string _description;
- private IProgress<ProgressInfo> _progress;
private readonly AbstractUIThreadOperationContext _context;
private int _completedItems;
private int _totalItems;
@@ -208,22 +264,22 @@ namespace Microsoft.VisualStudio.Utilities
public IUIThreadOperationContext Context => _context;
- public IProgress<ProgressInfo> Progress => _progress ?? (_progress = new Progress<ProgressInfo>((progressInfo) => OnProgressChanged(progressInfo)));
+ public IProgress<ProgressInfo> Progress => this;
public int CompletedItems => _completedItems;
public int TotalItems => _totalItems;
- private void OnProgressChanged(ProgressInfo progressInfo)
+ public void Dispose()
{
- Interlocked.Exchange(ref _completedItems, progressInfo.CompletedItems);
- Interlocked.Exchange(ref _totalItems, progressInfo.TotalItems);
- _context.OnScopeProgressChanged(this);
+ _context.OnScopeDisposed(this);
}
- public void Dispose()
+ void IProgress<ProgressInfo>.Report(ProgressInfo progressInfo)
{
- _context.OnScopeDisposed(this);
+ Interlocked.Exchange(ref _completedItems, progressInfo.CompletedItems);
+ Interlocked.Exchange(ref _totalItems, progressInfo.TotalItems);
+ _context.OnScopeProgressChanged(this);
}
}
}
diff --git a/src/Text/Def/TextUI/Utilities/IStatusBarService.cs b/src/Text/Def/TextUI/Utilities/IStatusBarService.cs
new file mode 100644
index 0000000..05dc1dd
--- /dev/null
+++ b/src/Text/Def/TextUI/Utilities/IStatusBarService.cs
@@ -0,0 +1,22 @@
+using System.Threading.Tasks;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// A status bar service enabling to send messages to the editor host's status bar.
+ /// </summary>
+ /// <remarks>
+ /// <para>This is a MEF component part, and should be imported as follows:
+ /// [Import]
+ /// IStatusBarService statusBarService = null;
+ /// </para>
+ /// </remarks>
+ internal interface IStatusBarService
+ {
+ /// <summary>
+ /// Sends a text to the editor host's status bar.
+ /// </summary>
+ /// <param name="text">A text to be displayed on the status bar.</param>
+ Task SetTextAsync(string text);
+ }
+}
diff --git a/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs b/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs
index b47b0b4..37804b2 100644
--- a/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs
+++ b/src/Text/Def/TextUI/Utilities/IUIThreadOperationExecutor.cs
@@ -64,7 +64,7 @@ namespace Microsoft.VisualStudio.Utilities
/// <summary>
/// Executes the action synchronously and waits for it to complete.
/// </summary>
- /// <param name="title">Operation's title.</param>
+ /// <param name="title">Operation's title. Can be null to indicate that the wait dialog should use the application's title.</param>
/// <param name="defaultDescription">Default operation's description, which is displayed on the wait dialog unless
/// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to
/// the <see cref="IUIThreadOperationContext"/>.</param>
@@ -76,10 +76,18 @@ namespace Microsoft.VisualStudio.Utilities
Action<IUIThreadOperationContext> action);
/// <summary>
+ /// Executes the action synchronously and waits for it to complete.
+ /// </summary>
+ /// <param name="executionOptions">Options that control action execution behavior.</param>
+ /// <param name="action">An action to execute.</param>
+ /// <returns>A status of action execution.</returns>
+ UIThreadOperationStatus Execute(UIThreadOperationExecutionOptions executionOptions, Action<IUIThreadOperationContext> action);
+
+ /// <summary>
/// Begins executing potentially long running operation on the caller thread and provides a context object that provides access to shared
/// cancellability and wait indication.
/// </summary>
- /// <param name="title">Operation's title.</param>
+ /// <param name="title">Operation's title. Can be null to indicate that the wait dialog should use the application's title.</param>
/// <param name="defaultDescription">Default operation's description, which is displayed on the wait dialog unless
/// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to
/// the <see cref="IUIThreadOperationContext"/>.</param>
@@ -89,5 +97,15 @@ namespace Microsoft.VisualStudio.Utilities
/// cancellability and wait indication for the given operation. The operation is considered executed
/// when this <see cref="IUIThreadOperationContext"/> instance is disposed.</returns>
IUIThreadOperationContext BeginExecute(string title, string defaultDescription, bool allowCancellation, bool showProgress);
+
+ /// <summary>
+ /// Begins executing potentially long running operation on the caller thread and provides a context object that provides access to shared
+ /// cancellability and wait indication.
+ /// </summary>
+ /// <param name="executionOptions">Options that control execution behavior.</param>
+ /// <returns><see cref="IUIThreadOperationContext"/> instance that provides access to shared two way
+ /// cancellability and wait indication for the given operation. The operation is considered executed
+ /// when this <see cref="IUIThreadOperationContext"/> instance is disposed.</returns>
+ IUIThreadOperationContext BeginExecute(UIThreadOperationExecutionOptions executionOptions);
}
}
diff --git a/src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs b/src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs
new file mode 100644
index 0000000..2472f4c
--- /dev/null
+++ b/src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs
@@ -0,0 +1,44 @@
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// A controller that enables and controls auto-cancellation of an operation execution by
+ /// <see cref="IUIThreadOperationExecutor"/> on a timeout.
+ /// </summary>
+ public interface IUIThreadOperationTimeoutController
+ {
+ /// <summary>
+ /// The duration (in milliseconds) after which an operation shouold be auto-cancelled.
+ /// </summary>
+ /// <remarks><see cref="Timeout.Infinite"/> disables auto-cancellation.</remarks>
+ int CancelAfter { get; }
+
+ /// <summary>
+ /// Gets whether an operation, whose execution time exceeded <see cref="CancelAfter"/> timeout should be
+ /// cancelled.
+ /// </summary>
+ /// <remarks>This callback can be used to disable auto-cancellation when an operation already
+ /// passed the point of no cancellation and it would leave system in an inconsistent state.
+ /// This method is called on a background thread.</remarks>
+ bool ShouldCancel();
+
+ /// <summary>
+ /// An event callback raised when an operation execution timeout was reached.
+ /// </summary>
+ /// <param name="wasExecutionCancelled">Indicates whether an operation was auto-cancelled.
+ /// Might be <c>false</c> if the operation is not cancellable (<see cref="IUIThreadOperationContext.AllowCancellation"/>
+ /// is <c>false</c> or <see cref="ShouldCancel"/> returned <c>false</c>.
+ /// </param>7
+ /// <remarks>This method is called on a background thread.</remarks>
+ void OnTimeout(bool wasExecutionCancelled);
+
+ /// <summary>
+ /// An event callback raised when a UI thread operation execution took long enough to be considered
+ /// as a delay. Visual Studio implementation of the <see cref="IUIThreadOperationExecutor"/> displays
+ /// a wait dialog at this point.
+ /// </summary>
+ /// <remarks>This method is called on a background thread.</remarks>
+ void OnDelay();
+ }
+}
diff --git a/src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs b/src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs
new file mode 100644
index 0000000..7a5cf8a
--- /dev/null
+++ b/src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Threading;
+
+namespace Microsoft.VisualStudio.Utilities
+{
+ /// <summary>
+ /// Options that control behavior of <see cref="IUIThreadOperationExecutor"/>.
+ /// </summary>
+ public class UIThreadOperationExecutionOptions
+ {
+ /// <summary>
+ /// Operation's title.
+ /// </summary>
+ public string Title { get; }
+
+ /// <summary>
+ /// Default operation's description, which is displayed on the wait dialog unless
+ /// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to
+ /// the <see cref="IUIThreadOperationContext"/>.
+ /// </summary>
+ public string DefaultDescription { get; }
+
+ /// <summary>
+ /// Whether to allow cancellability.
+ /// </summary>
+ public bool AllowCancellation { get; }
+
+ /// <summary>
+ /// Whether to show progress indication.
+ /// </summary>
+ public bool ShowProgress { get; }
+
+ /// <summary>
+ /// A controller that enables and controls auto-cancellation of an operation execution on a timeout.
+ /// </summary>
+ public IUIThreadOperationTimeoutController TimeoutController { get; }
+
+ /// <summary>
+ /// Creates a new instance of the <see cref="UIThreadOperationExecutionOptions"/>.
+ /// </summary>
+ /// <param name="title">Operation's title. Can be null to indicate that the wait dialog should use the application's title.</param>
+ /// <param name="defaultDescription">Default operation's description, which is displayed on the wait dialog unless
+ /// one or more <see cref="IUIThreadOperationScope"/>s with more specific descriptions were added to
+ /// the <see cref="IUIThreadOperationContext"/>.</param>
+ /// <param name="allowCancellation">Whether to allow cancellability.</param>
+ /// <param name="showProgress">Whether to show progress indication.</param>
+ /// <param name="timeoutController">A controller that enables and controls auto-cancellation of an operation execution on a timeout.</param>
+ public UIThreadOperationExecutionOptions(string title, string defaultDescription, bool allowCancellation, bool showProgress, IUIThreadOperationTimeoutController timeoutController = null)
+ {
+ Title = title;
+ DefaultDescription = defaultDescription ?? throw new ArgumentNullException(nameof(defaultDescription));
+ AllowCancellation = allowCancellation;
+ ShowProgress = showProgress;
+
+ // Timeout.Infinite is -1, other than that any negative value is invalid
+ if (timeoutController?.CancelAfter < Timeout.Infinite)
+ {
+ throw new ArgumentOutOfRangeException(nameof(timeoutController));
+ }
+
+ TimeoutController = timeoutController;
+ }
+ }
+}
diff --git a/src/Text/Impl/Commanding/CommandingStrings.Designer.cs b/src/Text/Impl/Commanding/CommandingStrings.Designer.cs
index 714059e..8d2e4db 100644
--- a/src/Text/Impl/Commanding/CommandingStrings.Designer.cs
+++ b/src/Text/Impl/Commanding/CommandingStrings.Designer.cs
@@ -19,7 +19,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation {
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class CommandingStrings {
@@ -61,6 +61,15 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation {
}
/// <summary>
+ /// Looks up a localized string similar to Command handler &apos;{0}&apos; has exceeded allotted timeout and was auto canceled..
+ /// </summary>
+ internal static string CommandCancelled {
+ get {
+ return ResourceManager.GetString("CommandCancelled", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Please wait for an editor command to finish....
/// </summary>
internal static string WaitForCommandExecution {
diff --git a/src/Text/Impl/Commanding/CommandingStrings.resx b/src/Text/Impl/Commanding/CommandingStrings.resx
index b05561d..9286e4d 100644
--- a/src/Text/Impl/Commanding/CommandingStrings.resx
+++ b/src/Text/Impl/Commanding/CommandingStrings.resx
@@ -101,4 +101,7 @@
<data name="WaitForCommandExecution" xml:space="preserve">
<value>Please wait for an editor command to finish...</value>
</data>
+ <data name="CommandCancelled" xml:space="preserve">
+ <value>Command handler '{0}' has exceeded allotted timeout and was auto canceled.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/src/Text/Impl/Commanding/EditorCommandHandlerService.cs b/src/Text/Impl/Commanding/EditorCommandHandlerService.cs
index 4ced5c3..c9730e7 100644
--- a/src/Text/Impl/Commanding/EditorCommandHandlerService.cs
+++ b/src/Text/Impl/Commanding/EditorCommandHandlerService.cs
@@ -1,27 +1,30 @@
using System;
-using System.Linq;
using System.Collections.Generic;
-using Microsoft.VisualStudio.Utilities;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Threading;
using Microsoft.VisualStudio.Commanding;
-using Microsoft.VisualStudio.Text.Utilities;
-using Microsoft.VisualStudio.Text.Editor.Commanding;
-using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text;
-using Microsoft.VisualStudio.Threading;
+using Microsoft.VisualStudio.Text.Editor;
+using Microsoft.VisualStudio.Text.Editor.Commanding;
+using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
+using Microsoft.VisualStudio.Text.Utilities;
+using Microsoft.VisualStudio.Utilities;
using ICommandHandlerAndMetadata = System.Lazy<Microsoft.VisualStudio.Commanding.ICommandHandler, Microsoft.VisualStudio.UI.Text.Commanding.Implementation.ICommandHandlerMetadata>;
-using System.Runtime.CompilerServices;
namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
internal class EditorCommandHandlerService : IEditorCommandHandlerService
{
+ private const string TelemetryEventPrefix = "VS/Editor/Commanding";
+ private const string TelemetryPropertyPrefix = "VS.Editor.Commanding";
+
private readonly IEnumerable<ICommandHandlerAndMetadata> _commandHandlers;
- private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
- private readonly JoinableTaskContext _joinableTaskContext;
+ private readonly EditorCommandHandlerServiceFactory _factory;
private readonly ITextView _textView;
- private readonly IComparer<IEnumerable<string>> _contentTypesComparer;
private readonly ICommandingTextBufferResolver _bufferResolver;
- private readonly IGuardedOperations _guardedOperations;
private readonly static IReadOnlyList<ICommandHandlerAndMetadata> EmptyHandlerList = new List<ICommandHandlerAndMetadata>(0);
private readonly static Action EmptyAction = delegate { };
@@ -32,26 +35,21 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
/// handlers every time we need handlers of a specific type, for a given content type.
private readonly Dictionary<(Type commandArgType, IContentType contentType), IReadOnlyList<ICommandHandlerAndMetadata>> _commandHandlersByTypeAndContentType;
- public EditorCommandHandlerService(ITextView textView,
+ public EditorCommandHandlerService(EditorCommandHandlerServiceFactory factory,
+ ITextView textView,
IEnumerable<ICommandHandlerAndMetadata> commandHandlers,
- IUIThreadOperationExecutor uiThreadOperationExecutor, JoinableTaskContext joinableTaskContext,
- IComparer<IEnumerable<string>> contentTypesComparer,
- ICommandingTextBufferResolver bufferResolver,
- IGuardedOperations guardedOperations)
+ ICommandingTextBufferResolver bufferResolver)
{
_commandHandlers = commandHandlers ?? throw new ArgumentNullException(nameof(commandHandlers));
+ _factory = factory ?? throw new ArgumentNullException(nameof(factory));
_textView = textView ?? throw new ArgumentNullException(nameof(textView));
- _uiThreadOperationExecutor = uiThreadOperationExecutor ?? throw new ArgumentNullException(nameof(uiThreadOperationExecutor));
- _joinableTaskContext = joinableTaskContext ?? throw new ArgumentNullException(nameof(joinableTaskContext));
- _contentTypesComparer = contentTypesComparer ?? throw new ArgumentNullException(nameof(contentTypesComparer));
_commandHandlersByTypeAndContentType = new Dictionary<(Type commandArgType, IContentType contentType), IReadOnlyList<ICommandHandlerAndMetadata>>();
_bufferResolver = bufferResolver ?? throw new ArgumentNullException(nameof(bufferResolver));
- _guardedOperations = guardedOperations ?? throw new ArgumentNullException(nameof(guardedOperations));
}
public CommandState GetCommandState<T>(Func<ITextView, ITextBuffer, T> argsFactory, Func<CommandState> nextCommandHandler) where T : EditorCommandArgs
{
- if (!_joinableTaskContext.IsOnMainThread)
+ if (!_factory.JoinableTaskContext.IsOnMainThread)
{
throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.GetCommandState)} method shoudl only be called on the UI thread.");
}
@@ -91,7 +89,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
public void Execute<T>(Func<ITextView, ITextBuffer, T> argsFactory, Action nextCommandHandler) where T : EditorCommandArgs
{
- if (!_joinableTaskContext.IsOnMainThread)
+ if (!_factory.JoinableTaskContext.IsOnMainThread)
{
throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.Execute)} method shoudl only be called on the UI thread.");
}
@@ -105,10 +103,10 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
return;
}
+ EditorCommandHandlerServiceState state = null;
+
using (var reentrancyGuard = new ReentrancyGuard(_textView))
{
- CommandExecutionContext commandExecutionContext = null;
-
// Build up chain of handlers per buffer
Action handlerChain = nextCommandHandler ?? EmptyAction;
// TODO: realize the chain dynamically and without Reverse()
@@ -123,6 +121,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
// Args factory failed, skip command handlers and just call next
handlerChain();
+ return;
}
if (handler is IDynamicCommandHandler<T> dynamicCommandHandler &&
@@ -132,29 +131,55 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
continue;
}
- if (commandExecutionContext == null)
+ if (state == null)
{
- commandExecutionContext = CreateCommandExecutionContext();
+ state = InitializeExecutionState(args);
}
- handlerChain = () => _guardedOperations.CallExtensionPoint(handler,
- () => handler.ExecuteCommand(args, nextHandler, commandExecutionContext),
+ handlerChain = () => _factory.GuardedOperations.CallExtensionPoint(handler,
+ () =>
+ {
+ state.OnExecutingCommandHandlerBegin(handler);
+ handler.ExecuteCommand(args, nextHandler, state.ExecutionContext);
+ state.OnExecutingCommandHandlerEnd(handler);
+ },
// Do not guard against cancellation exceptions, they are handled by ExecuteCommandHandlerChain
- exceptionGuardFilter: (e) => !IsOperationCancelledException(e));
+ exceptionGuardFilter: (e) => !IsOperationCancelledException(e));
}
- ExecuteCommandHandlerChain(commandExecutionContext, handlerChain, nextCommandHandler);
+ if (state == null)
+ {
+ // No matching command handlers, just call next
+ handlerChain();
+ return;
+ }
+
+ ExecuteCommandHandlerChain(state, handlerChain, nextCommandHandler);
}
}
+ private EditorCommandHandlerServiceState InitializeExecutionState<T>(T args) where T : EditorCommandArgs
+ {
+ var state = new EditorCommandHandlerServiceState(args, IsTypingCommand(args));
+ var uiThreadOperationContext = _factory.UIThreadOperationExecutor.BeginExecute(
+ new UIThreadOperationExecutionOptions(
+ title: null, // We want same caption as the main window
+ defaultDescription: WaitForCommandExecutionString, allowCancellation: true, showProgress: true,
+ timeoutController: new TimeoutController(state, _textView, _factory.LoggingService)));
+ var commandExecutionContext = new CommandExecutionContext(uiThreadOperationContext);
+ commandExecutionContext.OperationContext.UserCancellationToken.Register(OnExecutionCancellationRequested, state);
+ state.ExecutionContext = commandExecutionContext;
+ return state;
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsOperationCancelledException(Exception e)
{
return e is OperationCanceledException || e is AggregateException aggregate && aggregate.InnerExceptions.All(ie => ie is OperationCanceledException);
}
- private static void ExecuteCommandHandlerChain(
- CommandExecutionContext commandExecutionContext,
+ private void ExecuteCommandHandlerChain(
+ EditorCommandHandlerServiceState state,
Action handlerChain,
Action nextCommandHandler)
{
@@ -162,21 +187,47 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
// Kick off the first command handler.
handlerChain();
+ if (state.ExecutionContext.OperationContext.UserCancellationToken.IsCancellationRequested)
+ {
+ LogCancellationWasIgnored(state);
+ }
}
catch (OperationCanceledException)
{
- nextCommandHandler?.Invoke();
+ OnCommandExecutionCancelled(nextCommandHandler, state);
}
catch (AggregateException aggregate) when (aggregate.InnerExceptions.All(e => e is OperationCanceledException))
{
- nextCommandHandler?.Invoke();
+ OnCommandExecutionCancelled(nextCommandHandler, state);
}
finally
{
- commandExecutionContext?.OperationContext?.Dispose();
+ state.ExecutionContext?.OperationContext?.Dispose();
}
}
+ private void OnExecutionCancellationRequested(object state)
+ {
+ Debug.Assert(!_factory.JoinableTaskContext.IsOnMainThread);
+ ((EditorCommandHandlerServiceState)state).OnExecutionCancellationRequested();
+ }
+
+ private void OnCommandExecutionCancelled(Action nextCommandHandler, EditorCommandHandlerServiceState state)
+ {
+ var executingHandler = state.GetCurrentlyExecutingCommandHander();
+ var executingCommand = state.ExecutingCommand;
+ bool userCancelled = !state.ExecutionHasTimedOut;
+ _factory.JoinableTaskContext.Factory.RunAsync(async () =>
+ {
+ LogCommandExecutionCancelled(executingHandler, executingCommand, userCancelled);
+
+ string statusBarMessage = string.Format(CultureInfo.CurrentCulture, CommandingStrings.CommandCancelled, executingHandler?.DisplayName);
+ await _factory.StatusBar.SetTextAsync(statusBarMessage).ConfigureAwait(false);
+ });
+
+ nextCommandHandler?.Invoke();
+ }
+
private class ReentrancyGuard : IDisposable
{
private readonly IPropertyOwner _owner;
@@ -198,15 +249,6 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
return _textView.Properties.ContainsProperty(typeof(ReentrancyGuard));
}
- private CommandExecutionContext CreateCommandExecutionContext()
- {
- CommandExecutionContext commandExecutionContext;
- var uiThreadOperationContext = _uiThreadOperationExecutor.BeginExecute(title: null, // We want same caption as the main window
- defaultDescription: WaitForCommandExecutionString, allowCancellation: true, showProgress: true);
- commandExecutionContext = new CommandExecutionContext(uiThreadOperationContext);
- return commandExecutionContext;
- }
-
//internal for unit tests
internal IEnumerable<(ITextBuffer buffer, ICommandHandler handler)> GetOrderedBuffersAndCommandHandlers<T>() where T : EditorCommandArgs
{
@@ -289,7 +331,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
var handler = handlerBuckets[i].Peek();
// Can this handler handle content type more specific than top handler in firstNonEmptyBucket?
- if (_contentTypesComparer.Compare(handler.Metadata.ContentTypes, currentHandler.Metadata.ContentTypes) < 0)
+ if (_factory.ContentTypeComparer.Compare(handler.Metadata.ContentTypes, currentHandler.Metadata.ContentTypes) < 0)
{
foundBetterHandler = true;
handlerBuckets[i].Pop();
@@ -316,7 +358,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
IList<ICommandHandlerAndMetadata> newCommandHandlerList = null;
foreach (var lazyCommandHandler in SelectMatchingCommandHandlers(_commandHandlers, contentType, textViewRoles))
{
- var commandHandler = _guardedOperations.InstantiateExtension<ICommandHandler>(this, lazyCommandHandler);
+ var commandHandler = _factory.GuardedOperations.InstantiateExtension<ICommandHandler>(this, lazyCommandHandler);
if (commandHandler is ICommandHandler<T> || commandHandler is IChainedCommandHandler<T>)
{
if (newCommandHandlerList == null)
@@ -389,5 +431,82 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
return false;
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsTypingCommand(EditorCommandArgs args)
+ {
+ // TODO: temporarily only include typechar to not break Roslyn inline rename and other non-typing scenario, tracked by #657668
+ return args is TypeCharCommandArgs;
+ //args is DeleteKeyCommandArgs ||
+ //args is ReturnKeyCommandArgs ||
+ //args is BackspaceKeyCommandArgs ||
+ //args is TabKeyCommandArgs ||
+ //args is UndoCommandArgs ||
+ //args is RedoCommandArgs;
+ }
+
+ private void LogCommandExecutionCancelled(INamed executingHandler, EditorCommandArgs executingCommand, bool userCancelled)
+ {
+ _factory.LoggingService?.PostEvent($"{TelemetryEventPrefix}/ExecutionCancelled",
+ $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.CommandHandler", executingHandler?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.UserCancelled", userCancelled);
+ }
+
+ private void LogCancellationWasIgnored(EditorCommandHandlerServiceState state)
+ {
+ bool userCancelled = !state.ExecutionHasTimedOut;
+ var executingCommand = state.ExecutingCommand;
+ _factory.LoggingService?.PostEvent($"{TelemetryEventPrefix}/IgnoredExecutionCancellation",
+ $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.CommandHandler", state.CommandHandlerExecutingDuringCancellationRequest?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.UserCancelled", userCancelled);
+ }
+
+ private class TimeoutController : IUIThreadOperationTimeoutController
+ {
+ private readonly EditorCommandHandlerServiceState _state;
+ private readonly ITextView _textView;
+ private readonly ILoggingServiceInternal _loggingService;
+
+ public TimeoutController(EditorCommandHandlerServiceState state, ITextView textView, ILoggingServiceInternal loggingService)
+ {
+ _state = state;
+ _textView = textView;
+ _loggingService = loggingService;
+ }
+
+ public int CancelAfter
+ => _state.IsExecutingTypingCommand ?
+ _textView.Options.GetOptionValue(DefaultOptions.MaximumTypingLatencyOptionId) :
+ Timeout.Infinite;
+
+ public bool ShouldCancel()
+ {
+ // TODO: this needs to allow non typing command scenarios for example hitting return in inline rename, tracked by #657668
+ return _state.IsExecutingTypingCommand;
+ }
+
+ public void OnTimeout(bool wasExecutionCancelled)
+ {
+ Debug.Assert(_state.IsExecutingTypingCommand);
+ _state.ExecutionHasTimedOut = true;
+ var executingCommand = _state.ExecutingCommand;
+
+ _loggingService?.PostEvent($"{TelemetryEventPrefix}/ExecutionTimeout",
+ $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.CommandHandler", _state.GetCurrentlyExecutingCommandHander()?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.Timeout", this.CancelAfter,
+ $"{TelemetryPropertyPrefix}.WasExecutionCancelled", wasExecutionCancelled);
+ }
+
+ public void OnDelay()
+ {
+ var executingCommand = _state.ExecutingCommand;
+ _loggingService?.PostEvent($"{TelemetryEventPrefix}/WaitDialogShown",
+ $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName,
+ $"{TelemetryPropertyPrefix}.CommandHandler", _state.GetCurrentlyExecutingCommandHander()?.GetType().FullName);
+ }
+ }
}
}
diff --git a/src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs b/src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs
index f38a85c..a2eac95 100644
--- a/src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs
+++ b/src/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs
@@ -8,6 +8,7 @@ using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.Threading;
using System.Linq;
using Microsoft.VisualStudio.Text;
+using Microsoft.VisualStudio.Text.Utilities;
namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
@@ -16,11 +17,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
{
private readonly IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> _commandHandlers;
private readonly IList<Lazy<ICommandingTextBufferResolverProvider, IContentTypeMetadata>> _bufferResolverProviders;
- private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
- private readonly JoinableTaskContext _joinableTaskContext;
private readonly IContentTypeRegistryService _contentTypeRegistryService;
- private readonly IGuardedOperations _guardedOperations;
- private readonly StableContentTypeComparer _contentTypeComparer;
[ImportingConstructor]
public EditorCommandHandlerServiceFactory(
@@ -28,14 +25,19 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
[ImportMany]IEnumerable<Lazy<ICommandingTextBufferResolverProvider, IContentTypeMetadata>> bufferResolvers,
IUIThreadOperationExecutor uiThreadOperationExecutor,
JoinableTaskContext joinableTaskContext,
+ IStatusBarService statusBar,
IContentTypeRegistryService contentTypeRegistryService,
- IGuardedOperations guardedOperations)
+ IGuardedOperations guardedOperations,
+ [Import(AllowDefault = true)] ILoggingServiceInternal loggingService)
{
- _uiThreadOperationExecutor = uiThreadOperationExecutor;
- _joinableTaskContext = joinableTaskContext;
- _guardedOperations = guardedOperations;
+ UIThreadOperationExecutor = uiThreadOperationExecutor;
+ JoinableTaskContext = joinableTaskContext;
+ StatusBar = statusBar;
+ GuardedOperations = guardedOperations;
+ LoggingService = loggingService;
+
_contentTypeRegistryService = contentTypeRegistryService;
- _contentTypeComparer = new StableContentTypeComparer(_contentTypeRegistryService);
+ ContentTypeComparer = new StableContentTypeComparer(_contentTypeRegistryService);
_commandHandlers = OrderCommandHandlers(commandHandlers);
if (!bufferResolvers.Any())
{
@@ -45,16 +47,27 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
_bufferResolverProviders = bufferResolvers.ToList();
}
+ internal IGuardedOperations GuardedOperations { get; }
+
+ internal ILoggingServiceInternal LoggingService { get; }
+
+ internal JoinableTaskContext JoinableTaskContext { get; }
+
+ internal IUIThreadOperationExecutor UIThreadOperationExecutor { get; }
+
+ internal IStatusBarService StatusBar { get; }
+
+ internal StableContentTypeComparer ContentTypeComparer { get; }
+
public IEditorCommandHandlerService GetService(ITextView textView)
{
return textView.Properties.GetOrCreateSingletonProperty(() =>
{
- var bufferResolverProvider = _guardedOperations.InvokeBestMatchingFactory(_bufferResolverProviders, textView.TextBuffer.ContentType, _contentTypeRegistryService, errorSource: this);
+ var bufferResolverProvider = GuardedOperations.InvokeBestMatchingFactory(_bufferResolverProviders, textView.TextBuffer.ContentType, _contentTypeRegistryService, errorSource: this);
ICommandingTextBufferResolver bufferResolver = null;
- _guardedOperations.CallExtensionPoint(() => bufferResolver = bufferResolverProvider.CreateResolver(textView));
+ GuardedOperations.CallExtensionPoint(() => bufferResolver = bufferResolverProvider.CreateResolver(textView));
bufferResolver = bufferResolver ?? new DefaultBufferResolver(textView);
- return new EditorCommandHandlerService(textView, _commandHandlers, _uiThreadOperationExecutor, _joinableTaskContext,
- _contentTypeComparer, bufferResolver, _guardedOperations);
+ return new EditorCommandHandlerService(this, textView, _commandHandlers, bufferResolver);
});
}
@@ -69,14 +82,12 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
// buffer can be used by another text view, see https://devdiv.visualstudio.com/DevDiv/_workitems/edit/563472.
// There is no good way to cache it without holding onto the buffer (which can be disconnected
// from the text view anytime).
- return new EditorCommandHandlerService(textView, _commandHandlers, _uiThreadOperationExecutor,
- _joinableTaskContext, _contentTypeComparer,
- new SingleBufferResolver(subjectBuffer), _guardedOperations);
+ return new EditorCommandHandlerService(this, textView, _commandHandlers, new SingleBufferResolver(subjectBuffer));
}
private IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> OrderCommandHandlers(IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> commandHandlers)
{
- return commandHandlers.OrderBy((handler) => handler.Metadata.ContentTypes, _contentTypeComparer);
+ return commandHandlers.OrderBy((handler) => handler.Metadata.ContentTypes, ContentTypeComparer);
}
}
}
diff --git a/src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs b/src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs
new file mode 100644
index 0000000..d94ec07
--- /dev/null
+++ b/src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Collections.Concurrent;
+using System.Diagnostics;
+using Microsoft.VisualStudio.Commanding;
+using Microsoft.VisualStudio.Text.Editor.Commanding;
+using Microsoft.VisualStudio.Utilities;
+
+namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation
+{
+ internal class EditorCommandHandlerServiceState
+ {
+ private readonly ConcurrentStack<ICommandHandler> _executingCommandHandlers = new ConcurrentStack<ICommandHandler>();
+
+ public CommandExecutionContext ExecutionContext { get; set; }
+ public INamed CommandHandlerExecutingDuringCancellationRequest { get; set; }
+ public bool ExecutionHasTimedOut { get; set; }
+ public EditorCommandArgs ExecutingCommand { get; }
+ public bool IsExecutingTypingCommand { get; }
+
+ public EditorCommandHandlerServiceState(EditorCommandArgs executingCommand, bool isTypingCommand)
+ {
+ _executingCommandHandlers = new ConcurrentStack<ICommandHandler>();
+ ExecutingCommand = executingCommand ?? throw new ArgumentNullException(nameof(executingCommand));
+ IsExecutingTypingCommand = isTypingCommand;
+ }
+
+ public INamed GetCurrentlyExecutingCommandHander()
+ {
+ if (_executingCommandHandlers.TryPeek(out ICommandHandler handler) &&
+ handler is INamed namedHandler)
+ {
+ return namedHandler;
+ }
+
+ return null;
+ }
+
+ public void OnExecutingCommandHandlerBegin(ICommandHandler handler)
+ {
+ _executingCommandHandlers.Push(handler);
+ }
+
+ public void OnExecutingCommandHandlerEnd(ICommandHandler handler)
+ {
+ bool success = _executingCommandHandlers.TryPop(out var topCommandHandler);
+ Debug.Assert(success, "Unexpectedly empty command handler execution stack.");
+ Debug.Assert(handler == topCommandHandler, "Unexpected command hanlder on top of the stack.");
+ }
+
+ public void OnExecutionCancellationRequested()
+ {
+ CommandHandlerExecutingDuringCancellationRequest = GetCurrentlyExecutingCommandHander();
+ }
+ }
+}
diff --git a/src/Text/Impl/EditorOperations/EditorOperations.cs b/src/Text/Impl/EditorOperations/EditorOperations.cs
index 369273b..4672260 100644
--- a/src/Text/Impl/EditorOperations/EditorOperations.cs
+++ b/src/Text/Impl/EditorOperations/EditorOperations.cs
@@ -55,7 +55,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
ClearVirtualSpace
};
-#region Private Members
+ #region Private Members
ITextView _textView;
EditorOperationsFactoryService _factory;
@@ -80,7 +80,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
/// </summary>
private const string _boxSelectionCutCopyTag = "MSDEVColumnSelect";
-#endregion // Private Members
+ #endregion // Private Members
/// <summary>
/// Constructs an <see cref="EditorOperations"/> bound to a given <see cref="ITextView"/>.
@@ -121,7 +121,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
}
-#region IEditorOperations2 Members
+ #region IEditorOperations2 Members
public bool MoveSelectedLinesUp()
{
@@ -153,7 +153,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
endViewLine = _textView.GetTextViewLineContainingBufferPosition(_textView.Selection.End.Position - 1);
}
-#region Initial Asserts
+ #region Initial Asserts
Debug.Assert(_textView.Selection.Start.Position.Snapshot == _textView.TextSnapshot, "Selection is out of sync with view.");
@@ -161,7 +161,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
Debug.Assert(_textView.TextSnapshot == snapshot, "Text view lines are out of sync with the view");
-#endregion
+ #endregion
// check if we are at the top of the file, or trying to move a blank line
if (startLine.LineNumber < 1 || start == end)
@@ -363,7 +363,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
endViewLine = _textView.GetTextViewLineContainingBufferPosition(_textView.Selection.End.Position - 1);
}
-#region Initial Asserts
+ #region Initial Asserts
Debug.Assert(_textView.Selection.Start.Position.Snapshot == _textView.TextSnapshot, "Selection is out of sync with view.");
@@ -371,7 +371,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
Debug.Assert(_textView.TextSnapshot == snapshot, "Text view lines are out of sync with the view");
-#endregion
+ #endregion
// check if we are at the end of the file
if ((endLine.LineNumber + 1) >= snapshot.LineCount)
@@ -568,10 +568,10 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return line;
}
-#endregion
+ #endregion
-#region IEditorOperations Members
+ #region IEditorOperations Members
public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint)
{
@@ -1613,45 +1613,261 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
/// </summary>
public bool Indent()
{
- bool insertTabs = _textView.Selection.Mode == TextSelectionMode.Box || !IndentOperationShouldBeMultiLine;
+ if (!IndentWillCreateEdit())
+ {
+ // Indent box selection in virtual whitespace, if any.
+ if (_multiSelectionBroker.IsBoxSelection)
+ {
+ int indentSize = _editorOptions.GetIndentSize();
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxIndentForSelectionPoint(_multiSelectionBroker.BoxSelection.AnchorPoint, indentSize);
+ VirtualSnapshotPoint? activePoint = CalculateBoxIndentForSelectionPoint(_multiSelectionBroker.BoxSelection.ActivePoint, indentSize);
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+ }
+
+ return true;
+ }
Func<bool> action = () =>
{
- if (insertTabs)
+ using (_multiSelectionBroker.BeginBatchOperation())
{
- return this.EditHelper(edit =>
+ if (_multiSelectionBroker.IsBoxSelection)
{
- int tabSize = _editorOptions.GetTabSize();
- int indentSize = _editorOptions.GetIndentSize();
- bool convertTabsToSpaces = _editorOptions.IsConvertTabsToSpacesEnabled();
- bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box &&
- _textView.Selection.Start != _textView.Selection.End;
+ if (!TryIndentBoxSelection())
+ {
+ return false;
+ }
+ }
+ else
+ {
+ if (!TryIndentAndFixupStreamSelections())
+ {
+ return false;
+ }
+ }
- // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
- // through a text change.
- VirtualSnapshotPoint? anchorPoint = (boxSelection) ? CalculateBoxIndentForSelectionPoint(_textView.Selection.AnchorPoint, indentSize) : null;
- VirtualSnapshotPoint? activePoint = (boxSelection) ? CalculateBoxIndentForSelectionPoint(_textView.Selection.ActivePoint, indentSize) : null;
+ _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll);
+
+ return true;
+ }
+ };
+
+ return ExecuteAction(Strings.InsertTab, action, SelectionUpdate.Ignore, ensureVisible: false);
+ }
- // Insert an indent for each portion of the selection (with an empty selection, there will only be a single
- // span).
- foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ private bool IndentWillCreateEdit()
+ {
+ var allSelections = _multiSelectionBroker.AllSelections;
+ if (_multiSelectionBroker.IsBoxSelection)
+ {
+ // Box selection can only create an edit if at least one non-virtual subspan is of non-zero length.
+ for (int i = 0; i < allSelections.Count; i++)
+ {
+ var selection = allSelections[i];
+ if (!selection.Start.IsInVirtualSpace)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ for (int i = 0; i < allSelections.Count; i++)
+ {
+ var selection = allSelections[i];
+
+ // Carets (zero-width selections) always produce an edit.
+ if (selection.IsEmpty)
+ {
+ return true;
+ }
+ else
+ {
+ var startLine = selection.Start.Position.GetContainingLine();
+ var endLine = selection.End.Position.GetContainingLine();
+
+ // Selections that start and end on the same line create an edit if and only if the line is non-zero in length.
+ if (startLine.LineNumber == endLine.LineNumber)
+ {
+ return (startLine.Length > 0);
+ }
+ else
+ {
+ // Only edits for multiline selections if one of the contained lines is
+ // non-zero in length because empty lines have nothing to indent.
+ for (int j = startLine.LineNumber; j < endLine.LineNumber; j++)
{
- if (!InsertIndentForSpan(span, edit, exactlyOneIndentLevel: false))
- return false;
+ if (startLine.Snapshot.GetLineFromLineNumber(j).Length > 0)
+ {
+ return true;
+ }
}
+ }
+ }
+ }
- FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+ return false;
+ }
- return true;
- });
+ private bool TryIndentAndFixupStreamSelections()
+ {
+ var newSelections = new FrugalList<(Selection old, Selection newSel)>();
+ if (!EditHelper((edit) => IndentStreamSelections(edit, newSelections)))
+ {
+ return false;
+ }
+
+ _multiSelectionBroker.PerformActionOnAllSelections(transformer =>
+ {
+ var selection = transformer.Selection;
+ if (selection.IsEmpty && selection.Extent.IsInVirtualSpace)
+ {
+ // Remove all virtual space from selection.
+ transformer.MoveTo(
+ new VirtualSnapshotPoint(selection.AnchorPoint.Position),
+ new VirtualSnapshotPoint(selection.ActivePoint.Position),
+ new VirtualSnapshotPoint(selection.InsertionPoint.Position),
+ PositionAffinity.Successor);
+ }
+ });
+
+ // Apply selection changes after the edit so we can apply the desired point tracking mode.
+ // Preserves pre-multicaret behavior by keeping the left edge of selections unmodified
+ // when the user presses 'tab' using negative tracking.
+ TransformSelections(newSelections);
+
+ return true;
+ }
+
+ private void TransformSelections(FrugalList<(Selection old, Selection newSel)> newSelections)
+ {
+ foreach (var (oldSelection, newSelection) in newSelections)
+ {
+ var start = newSelection.Start.TranslateTo(_multiSelectionBroker.CurrentSnapshot, PointTrackingMode.Negative);
+ var end = newSelection.End.TranslateTo(_multiSelectionBroker.CurrentSnapshot, PointTrackingMode.Positive);
+ var insertion = newSelection.InsertionPoint.TranslateTo(
+ _multiSelectionBroker.CurrentSnapshot,
+ newSelection.IsReversed ? PointTrackingMode.Negative : PointTrackingMode.Positive);
+
+ _multiSelectionBroker.TryPerformActionOnSelection(
+ oldSelection,
+ transformer => transformer.MoveTo(
+ newSelection.IsReversed ? end : start,
+ newSelection.IsReversed ? start : end,
+ insertion,
+ newSelection.InsertionPointAffinity),
+ out _);
+ }
+ }
+
+ private bool IndentStreamSelections(ITextEdit edit, IList<(Selection old, Selection newSel)> newSelections)
+ {
+ // Tracks whether or not an edit succeeded. Edits can fail if the caret is in a readonly region.
+ bool succeeded = true;
+
+ // Compound edits perform multiple actions with reference to the original snapshot. This tracks
+ // the column adjustment of the previous caret so that we can align the subsequent caret with the
+ // correct tabstop when there are multiple carets on the same line.
+ var columnOffset = default(int?);
+
+ // There is a particular edge case where there can be multiple multi-line selections on the same line.
+ // Naively indenting one tabstop per selection would cause this line to be indented twice so we must
+ // keep track of which line the previous multi-line selection ended on.
+ int? previousSelectionEndLineNumber = 0;
+ bool previousSelectionWasMultiline = false;
+
+ _multiSelectionBroker.PerformActionOnAllSelections((transformer) =>
+ {
+ int? newSelectionStartLineNumber = transformer.Selection.Start.Position.GetContainingLine().LineNumber;
+ int? newSelectionEndLineNumber = transformer.Selection.End.Position.GetContainingLine().LineNumber;
+
+ // Reset the column offset if we've transitioned to the next line.
+ if (newSelectionStartLineNumber != previousSelectionEndLineNumber)
+ {
+ columnOffset = null;
+ }
+
+ previousSelectionWasMultiline = IndentOperationShouldBeMultiLine(transformer.Selection);
+
+ if (IndentOperationShouldBeMultiLine(transformer.Selection))
+ {
+ // Don't indent this line if we are the second multi-line selection intersecting this line
+ // or else we'll cause a double-indent.
+ bool skipFirstLine = (columnOffset != null) && previousSelectionEndLineNumber == newSelectionStartLineNumber && previousSelectionWasMultiline;
+
+ // In case there are multiple carets on a single line, we have to remember the column
+ // to ensure that the subsequent carets on the same line receive adequate indentation.
+ columnOffset = PerformIndentActionOnEachBufferLine(
+ edit,
+ newSelections,
+ skipFirstLine ? MultiLineSelectionWithoutFirstLine(transformer.Selection) : transformer.Selection,
+ InsertSingleIndentAtPoint);
+ if (columnOffset == null)
+ {
+ succeeded = false;
+ return;
+ }
}
else
{
- return PerformIndentActionOnEachBufferLine(InsertSingleIndentAtPoint);
+ // In case there are multiple carets on a single line, we have to remember the column
+ // to ensure that the subsequent carets on the same line receive adequate indentation.
+ columnOffset = InsertIndentForSpan(transformer.Selection.Extent, edit, exactlyOneIndentLevel: false, false, columnOffset: columnOffset ?? 0);
+ if (columnOffset == null)
+ {
+ succeeded = false;
+ return;
+ }
}
- };
- return ExecuteAction(Strings.InsertTab, action, (_textView.Selection.IsEmpty) ? SelectionUpdate.ClearVirtualSpace : SelectionUpdate.Ignore, ensureVisible: insertTabs);
+ previousSelectionEndLineNumber = newSelectionEndLineNumber;
+ });
+
+ return succeeded;
+ }
+
+ private static Selection MultiLineSelectionWithoutFirstLine(Selection selection)
+ {
+ var snapshot = selection.Start.Position.Snapshot;
+ var firstLineNumber = selection.Start.Position.GetContainingLine().LineNumber;
+
+ // Requires the selection to be multiple lines.
+ Debug.Assert(firstLineNumber <= snapshot.LineCount);
+
+ var start = new VirtualSnapshotPoint(snapshot.GetLineFromLineNumber(firstLineNumber + 1).Start);
+
+ return new Selection(
+ new VirtualSnapshotSpan(start, selection.End),
+ isReversed: selection.IsReversed);
+ }
+
+ private bool TryIndentBoxSelection()
+ {
+ return this.EditHelper((edit) =>
+ {
+ int tabSize = _editorOptions.GetTabSize();
+ int indentSize = _editorOptions.GetIndentSize();
+ bool convertTabsToSpaces = _editorOptions.IsConvertTabsToSpacesEnabled();
+
+ // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
+ // through a text change.
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxIndentForSelectionPoint(_multiSelectionBroker.BoxSelection.AnchorPoint, indentSize);
+ VirtualSnapshotPoint? activePoint = CalculateBoxIndentForSelectionPoint(_multiSelectionBroker.BoxSelection.ActivePoint, indentSize);
+
+ // Insert an indent for each portion of the selection (with an empty selection, there will only be a single
+ // span).
+ foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ {
+ if (InsertIndentForSpan(span, edit, exactlyOneIndentLevel: false) == null)
+ {
+ return false;
+ }
+ }
+
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+
+ return true;
+ });
}
/// <summary>
@@ -1661,58 +1877,158 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
/// </summary>
public bool Unindent()
{
- bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box &&
- _textView.Selection.Start != _textView.Selection.End;
+ if (UnindentWillCreateEdit())
+ {
+ Func<bool> action = () =>
+ {
+ using (_multiSelectionBroker.BeginBatchOperation())
+ {
+ var succeeded = this.EditHelper(edit =>
+ {
+ if (_multiSelectionBroker.IsBoxSelection)
+ {
+ return UnindentBoxSelection(edit);
+ }
+ else
+ {
+ return UnindentStreamSelections(edit);
+ }
+ });
- Func<bool> action = null;
+ _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll);
- if (_textView.Caret.InVirtualSpace && _textView.Selection.IsEmpty)
+ return succeeded;
+ }
+ };
+
+ return ExecuteAction(Strings.RemovePreviousTab, action, SelectionUpdate.Ignore, ensureVisible: true);
+ }
+ else
{
- this.MoveCaretToPreviousIndentStopInVirtualSpace();
+ using (_multiSelectionBroker.BeginBatchOperation())
+ {
+ if (_multiSelectionBroker.IsBoxSelection)
+ {
+ int columnsToRemove = DetermineMaxBoxUnindent();
+
+ // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
+ // through a text change.
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.AnchorPoint, columnsToRemove);
+ VirtualSnapshotPoint? activePoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.ActivePoint, columnsToRemove);
+
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+ }
+ else
+ {
+ _multiSelectionBroker.PerformActionOnAllSelections(transformer =>
+ {
+ VirtualSnapshotPoint point = GetPreviousIndentStopInVirtualSpace(transformer.Selection.Start);
+ transformer.MoveTo(point, select: false, PositionAffinity.Successor);
+ });
+ }
+ }
+
+ _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll);
return true;
}
- else if (!boxSelection && IndentOperationShouldBeMultiLine)
+ }
+
+ private bool UnindentWillCreateEdit()
+ {
+ var allSelections = _multiSelectionBroker.AllSelections;
+ if (_multiSelectionBroker.IsBoxSelection)
{
- action = () => PerformIndentActionOnEachBufferLine(RemoveIndentAtPoint);
+ // Box selection can only create an edit if at least one non-virtual subspan is of non-zero length.
+ for (int i = 0; i < allSelections.Count; i++)
+ {
+ var selection = allSelections[i];
+ if (!selection.Start.IsInVirtualSpace)
+ {
+ return true;
+ }
+ }
}
- else if (!boxSelection)
+ else
{
- action = () => EditHelper(edit => RemoveIndentAtPoint(_textView.Selection.Start.Position, edit, failOnNonWhitespaceCharacter: false));
+ // Stream selection can only create an edit if at least one selection has an end-point in non-virtual space.
+ for (int i = 0; i < allSelections.Count; i++)
+ {
+ var selection = allSelections[i];
+ if (!selection.Start.IsInVirtualSpace || !selection.End.IsInVirtualSpace)
+ {
+ return true;
+ }
+ }
}
- else // Box selection
+
+ return false;
+ }
+
+ private bool UnindentStreamSelections(ITextEdit edit)
+ {
+ bool succeeded = true;
+ _multiSelectionBroker.PerformActionOnAllSelections(transformer =>
{
- int columnsToRemove = DetermineMaxBoxUnindent();
+ Selection selection = transformer.Selection;
- action = () => EditHelper(edit =>
+ if (selection.IsEmpty && selection.InsertionPoint.IsInVirtualSpace)
{
- // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
- // through a text change.
- VirtualSnapshotPoint? anchorPoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.AnchorPoint, columnsToRemove);
- VirtualSnapshotPoint? activePoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.ActivePoint, columnsToRemove);
-
- // Remove an indent for each portion of the selection (with an empty selection, there will only be a single
- // span).
- foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ VirtualSnapshotPoint point = GetPreviousIndentStopInVirtualSpace(selection.InsertionPoint);
+ transformer.MoveTo(point, select: false, PositionAffinity.Successor);
+ }
+ else if (IndentOperationShouldBeMultiLine(selection))
+ {
+ var selections = new List<(Selection oldSelection, Selection newSelection)>();
+ if (PerformIndentActionOnEachBufferLine(edit, selections, selection, RemoveIndentAtPoint) == null)
{
- if (!RemoveIndentAtPoint(span.Start.Position, edit, failOnNonWhitespaceCharacter: false, columnsToRemove: columnsToRemove))
- return false;
+ succeeded = false;
+ return;
}
+ }
+ else
+ {
+ if (!RemoveIndentAtPoint(selection.Start.Position, edit, failOnNonWhitespaceCharacter: false))
+ {
+ succeeded = false;
+ return;
+ }
+ }
+ });
- FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+ return succeeded;
+ }
- return true;
+ private bool UnindentBoxSelection(ITextEdit edit)
+ {
+ int columnsToRemove = DetermineMaxBoxUnindent();
- });
+ // We'll need to update the start/end points if they are in virtual space, since they won't be tracking
+ // through a text change.
+ VirtualSnapshotPoint? anchorPoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.AnchorPoint, columnsToRemove);
+ VirtualSnapshotPoint? activePoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.ActivePoint, columnsToRemove);
+
+ // Remove an indent for each portion of the selection (with an empty selection, there will only be a single
+ // span).
+ foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans)
+ {
+ if (!RemoveIndentAtPoint(span.Start.Position, edit, failOnNonWhitespaceCharacter: false, columnsToRemove: columnsToRemove))
+ return false;
}
- return ExecuteAction(Strings.RemovePreviousTab, action, SelectionUpdate.Ignore, ensureVisible: true);
+ FixUpSelectionAfterBoxOperation(anchorPoint, activePoint);
+
+ return true;
}
public bool IncreaseLineIndent()
{
Func<bool> action = () =>
{
+ // NOTE: This method doesn't account for multiple multi-line selections indenting the same
+ // line, so using this method will double indent the line intersected by both selections.
+ // Not worth fixing at the moment because this isn't a mapped command. See Indent for an
+ // example of how this should work.
return PerformIndentActionOnEachBufferLine(InsertSingleIndentAtPoint);
};
@@ -2973,9 +3289,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
#endif
}
-#endregion // IEditorOperations Members
+ #endregion // IEditorOperations Members
-#region Virtual Space to Whitespace helpers
+ #region Virtual Space to Whitespace helpers
public string GetWhitespaceForVirtualSpace(VirtualSnapshotPoint point)
{
@@ -3064,9 +3380,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return textToInsert;
}
-#endregion
+ #endregion
-#region Text insertion helpers
+ #region Text insertion helpers
private bool InsertText(string text, bool final)
{
@@ -3502,9 +3818,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return succeeded;
}
-#endregion
+ #endregion
-#region Clipboard and RTF helpers
+ #region Clipboard and RTF helpers
private Func<bool> PrepareClipboardSelectionCopy()
{
@@ -3603,24 +3919,27 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
private string GenerateRtf(NormalizedSnapshotSpanCollection spans)
{
#if WINDOWS
- //Don't generate RTF for large spans (since it is expensive and probably not wanted).
- int length = spans.Sum((span) => span.Length);
- if (length < _textView.Options.GetOptionValue(MaxRtfCopyLength.OptionKey))
+ if (_textView.Options.GetOptionValue(EnableRtfCopy.OptionKey))
{
- if (_textView.Options.GetOptionValue(UseAccurateClassificationForRtfCopy.OptionKey))
+ //Don't generate RTF for large spans (since it is expensive and probably not wanted).
+ int length = spans.Sum((span) => span.Length);
+ if (length < _textView.Options.GetOptionValue(MaxRtfCopyLength.OptionKey))
{
- using (var dialog = WaitHelper.Wait(_factory.WaitIndicator, Strings.WaitTitle, Strings.WaitMessage))
+ if (_textView.Options.GetOptionValue(UseAccurateClassificationForRtfCopy.OptionKey))
{
- return ((IRtfBuilderService2)(_factory.RtfBuilderService)).GenerateRtf(spans, dialog.CancellationToken);
+ using (var dialog = WaitHelper.Wait(_factory.WaitIndicator, Strings.WaitTitle, Strings.WaitMessage))
+ {
+ return ((IRtfBuilderService2)(_factory.RtfBuilderService)).GenerateRtf(spans, dialog.CancellationToken);
+ }
+ }
+ else
+ {
+ return _factory.RtfBuilderService.GenerateRtf(spans);
}
}
- else
- {
- return _factory.RtfBuilderService.GenerateRtf(spans);
- }
- }
else
return null;
+ }
#else
return null;
@@ -3632,9 +3951,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return GenerateRtf(new NormalizedSnapshotSpanCollection(span));
}
-#endregion
+ #endregion
-#region Horizontal whitespace helpers
+ #region Horizontal whitespace helpers
private bool DeleteHorizontalWhitespace()
{
@@ -3816,22 +4135,60 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return startPoint;
}
-#endregion
+ #endregion
-#region Indent/unindent helpers
+ #region Indent/unindent helpers
// Perform the given indent action (indent/unindent) on each line at the first non-whitespace
// character, skipping lines that are either empty or just whitespace.
// This method is used by Indent, Unindent, IncreaseLineIndent, and DecreaseLineIndent, and
// is essentially a replacement for the editor primitive's Indent and Unindent functions.
- private bool PerformIndentActionOnEachBufferLine(Func<SnapshotPoint, ITextEdit, bool> action)
+ private bool PerformIndentActionOnEachBufferLine(Func<SnapshotPoint, ITextEdit, int?> action)
+ {
+ Func<bool> indentAction = () =>
+ {
+ var newSelections = new FrugalList<(Selection old, Selection newSel)>();
+ if (!this.EditHelper(edit =>
+ {
+ bool succeeded = true;
+
+ using (_multiSelectionBroker.BeginBatchOperation())
+ {
+ _multiSelectionBroker.PerformActionOnAllSelections(transformer =>
+ {
+ if (this.PerformIndentActionOnEachBufferLine(edit, newSelections, transformer.Selection, action) == null)
+ {
+ succeeded = false;
+ }
+ });
+ }
+
+ return succeeded;
+ }))
+ {
+ return false;
+ }
+
+ // Apply selection changes after the edit so we can apply the desired point tracking mode.
+ TransformSelections(newSelections);
+
+ _multiSelectionBroker.TryEnsureVisible(_multiSelectionBroker.PrimarySelection, EnsureSpanVisibleOptions.MinimumScroll);
+
+ return true;
+ };
+
+ return this.ExecuteAction(Strings.IncreaseLineIndent, indentAction, SelectionUpdate.Ignore);
+ }
+
+ private int? PerformIndentActionOnEachBufferLine(ITextEdit textEdit, IList<(Selection old, Selection newSel)> newSelections, Selection selection, Func<SnapshotPoint, ITextEdit, int?> action)
{
- Func<ITextEdit, bool> editAction = edit =>
+ Func<ITextEdit, int?> editAction = edit =>
{
+ int? actionResult = null;
ITextSnapshot snapshot = _textView.TextSnapshot;
- int startLineNumber = snapshot.GetLineNumberFromPosition(_textView.Selection.Start.Position);
- int endLineNumber = snapshot.GetLineNumberFromPosition(_textView.Selection.End.Position);
+ int startLineNumber = snapshot.GetLineNumberFromPosition(selection.Start.Position);
+ int endLineNumber = snapshot.GetLineNumberFromPosition(selection.End.Position);
for (int i = startLineNumber; i <= endLineNumber; i++)
{
@@ -3840,7 +4197,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
// If the line is blank or the (non-empty) selection ends at the start of this line, exclude
// the line from processing.
if (line.Length == 0 ||
- (!_textView.Selection.IsEmpty && line.Start == _textView.Selection.End.Position))
+ (!selection.IsEmpty && line.Start == selection.End.Position))
continue;
TextPoint textPoint = _editorPrimitives.Buffer.GetTextPoint(line.Start).GetFirstNonWhiteSpaceCharacterOnLine();
@@ -3850,67 +4207,73 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
SnapshotPoint point = new SnapshotPoint(snapshot, textPoint.CurrentPosition);
- if (!action(point, edit))
- return false;
+ actionResult = action(point, edit);
+ if (actionResult == null)
+ {
+ return null;
+ }
}
- return true;
+ // Return the indent of the final line.
+ return actionResult;
};
// Track the start of the selection with PointTrackingMode.Negative through the text
// change. If the result has the same absolute snapshot offset as the point before the change, we'll move
// the selection start point back to there instead of letting it track automatically.
- ITrackingPoint startPoint = _textView.TextSnapshot.CreateTrackingPoint(_textView.Selection.Start.Position, PointTrackingMode.Negative);
- int startPositionBufferChange = _textView.Selection.Start.Position;
+ ITrackingPoint startPoint = _textView.TextSnapshot.CreateTrackingPoint(selection.Start.Position, PointTrackingMode.Negative);
+ int startPositionBufferChange = selection.Start.Position;
- if (!EditHelper(editAction))
- return false;
+ var result = editAction.Invoke(textEdit);
+ if (result == null)
+ {
+ return null;
+ }
VirtualSnapshotPoint newStart = new VirtualSnapshotPoint(startPoint.GetPoint(_textView.TextSnapshot));
if (newStart.Position == startPositionBufferChange)
{
- bool isReversed = _textView.Selection.IsReversed;
- VirtualSnapshotPoint anchor = isReversed ? _textView.Selection.End : newStart;
- VirtualSnapshotPoint active = isReversed ? newStart : _textView.Selection.End;
- SelectAndMoveCaret(anchor, active);
+ bool isReversed = selection.IsReversed;
+ VirtualSnapshotPoint anchor = isReversed ? selection.End : newStart;
+ VirtualSnapshotPoint active = isReversed ? newStart : selection.End;
+
+ newSelections.Add(
+ (selection,
+ new Selection(
+ active.TranslateTo(_textView.TextSnapshot, PointTrackingMode.Positive),
+ anchor.TranslateTo(_textView.TextSnapshot, PointTrackingMode.Negative),
+ active.TranslateTo(_textView.TextSnapshot, PointTrackingMode.Positive),
+ selection.InsertionPointAffinity)));
}
- return true;
+ return result;
}
- // This is used by indent/unindent should be multiline operations. To be multiline, the selection
- // points must be on separate lines, and not just an entire line (though not the entire last line, which
- // we special case). This is for backwards compatibility with Orcas, but is generally undesirable behavior.
- // Dev10 #856382 tracks removing this special behavior for indent and just treating it like a tab at the
- // start of the selection.
- private bool IndentOperationShouldBeMultiLine
+ private bool IndentOperationShouldBeMultiLine(Selection selection)
{
- get
- {
- if (_textView.Selection.IsEmpty)
- return false;
+ if (selection.IsEmpty)
+ return false;
- var startLine = _textView.Selection.Start.Position.GetContainingLine();
+ var startLine = selection.Start.Position.GetContainingLine();
- bool pointsOnSameLine = _textView.Selection.End.Position <= startLine.End;
+ bool pointsOnSameLine = selection.End.Position <= startLine.End;
- bool lastLineOfFile = startLine.End == startLine.EndIncludingLineBreak;
- bool entireLastLineSelected = lastLineOfFile &&
- _textView.Selection.Start.Position == startLine.Start &&
- _textView.Selection.End.Position == startLine.End;
+ bool lastLineOfFile = startLine.End == startLine.EndIncludingLineBreak;
+ bool entireLastLineSelected = lastLineOfFile &&
+ _textView.Selection.Start.Position == startLine.Start &&
+ _textView.Selection.End.Position == startLine.End;
- return !pointsOnSameLine || entireLastLineSelected;
- }
+ return !pointsOnSameLine || entireLastLineSelected;
}
- private bool InsertSingleIndentAtPoint(SnapshotPoint point, ITextEdit edit)
+ private int? InsertSingleIndentAtPoint(SnapshotPoint point, ITextEdit edit)
{
VirtualSnapshotPoint virtualPoint = new VirtualSnapshotPoint(point);
VirtualSnapshotSpan span = new VirtualSnapshotSpan(virtualPoint, virtualPoint);
- return InsertIndentForSpan(span, edit, exactlyOneIndentLevel: true, useBufferPrimitives: true);
+ return InsertIndentForSpan(span, edit, exactlyOneIndentLevel: true, useBufferPrimitives: true) != null ? 0 : default(int?);
}
- private bool InsertIndentForSpan(VirtualSnapshotSpan span, ITextEdit edit, bool exactlyOneIndentLevel, bool useBufferPrimitives = false)
+ private int? InsertIndentForSpan(VirtualSnapshotSpan span, ITextEdit edit, bool exactlyOneIndentLevel, bool useBufferPrimitives = false, int columnOffset = 0)
{
int indentSize = _textView.Options.GetIndentSize();
bool convertTabsToSpaces = _textView.Options.IsConvertTabsToSpacesEnabled();
@@ -3923,7 +4286,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
// In a box selection, we don't insert anything for lines in virtual space
if (boxSelection && point.IsInVirtualSpace)
{
- return true;
+ return 0;
}
string textToInsert;
@@ -3972,15 +4335,13 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
else
textPoint = _editorPrimitives.View.GetTextPoint(point.Position.Position);
- currentColumn = textPoint.Column + point.VirtualSpaces;
+ currentColumn = textPoint.Column + point.VirtualSpaces + columnOffset;
if (exactlyOneIndentLevel)
distanceToNextIndentStop = indentSize;
else
distanceToNextIndentStop = indentSize - (currentColumn % indentSize);
- int columnToInsertTo = currentColumn + distanceToNextIndentStop;
-
textToInsert = GetWhiteSpaceForPositionAndVirtualSpace(point.Position, point.VirtualSpaces + distanceToNextIndentStop, useBufferPrimitives);
}
@@ -3999,12 +4360,15 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
}
}
- return edit.Replace(Span.FromBounds(startPointForReplace, endPointForReplace), textToInsert);
+ var spanToReplace = Span.FromBounds(startPointForReplace, endPointForReplace);
+ int newColumnOffset = (textToInsert.Length - spanToReplace.Length) + columnOffset;
+
+ return edit.Replace(spanToReplace, textToInsert) ? newColumnOffset : default(int?);
}
- private bool RemoveIndentAtPoint(SnapshotPoint point, ITextEdit edit)
+ private int? RemoveIndentAtPoint(SnapshotPoint point, ITextEdit edit)
{
- return RemoveIndentAtPoint(point, edit, failOnNonWhitespaceCharacter: true, useBufferPrimitives: true);
+ return RemoveIndentAtPoint(point, edit, failOnNonWhitespaceCharacter: true, useBufferPrimitives: true) ? 0 : default(int?);
}
/// <summary>
@@ -4088,14 +4452,6 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return edit.Replace(Span.FromBounds(startPointForReplace, point.Position), textToInsert);
}
- private void MoveCaretToPreviousIndentStopInVirtualSpace()
- {
- Debug.Assert(_textView.Caret.InVirtualSpace);
-
- VirtualSnapshotPoint point = GetPreviousIndentStopInVirtualSpace(_textView.Caret.Position.VirtualBufferPosition);
- _textView.Caret.MoveTo(point);
- }
-
/// <summary>
/// Used by the un-indenting logic to determine what an unindent means in virtual space.
/// </summary>
@@ -4118,9 +4474,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return new VirtualSnapshotPoint(point.Position);
}
-#endregion
+ #endregion
-#region Box Selection indent/unindent helpers
+ #region Box Selection indent/unindent helpers
/// <summary>
/// Given a "fix-up" anchor/active point determined before the box operation, fix up the current selection's
@@ -4234,9 +4590,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return maxColumnUnindent;
}
-#endregion
+ #endregion
-#region Miscellaneous line helpers
+ #region Miscellaneous line helpers
private DisplayTextRange GetFullLines()
{
@@ -4300,9 +4656,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return firstTextColumn.CurrentPosition == displayTextPoint.EndOfViewLine;
}
-#endregion
+ #endregion
-#region Tabs <-> spaces
+ #region Tabs <-> spaces
private bool ConvertSpacesAndTabsHelper(bool toTabs)
{
@@ -4427,9 +4783,9 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
return true;
}
-#endregion
+ #endregion
-#region Edit/Replace/Delete helpers
+ #region Edit/Replace/Delete helpers
internal bool EditHelper(Func<ITextEdit, bool> editAction)
{
@@ -4506,7 +4862,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
});
}
-#endregion
+ #endregion
internal bool IsEmptyBoxSelection()
{
@@ -4893,6 +5249,21 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation
}
[Export(typeof(EditorOptionDefinition))]
+ [Name(EnableRtfCopy.OptionName)]
+ public sealed class EnableRtfCopy : EditorOptionDefinition<bool>
+ {
+ public const string OptionName = "EnableRtfCopy";
+ public static readonly EditorOptionKey<bool> OptionKey = new EditorOptionKey<bool>(EnableRtfCopy.OptionName);
+
+ public override bool Default { get { return true; } }
+
+ /// <summary>
+ /// Gets the editor option key.
+ /// </summary>
+ public override EditorOptionKey<bool> Key { get { return EnableRtfCopy.OptionKey; } }
+ }
+
+ [Export(typeof(EditorOptionDefinition))]
[Name(UseAccurateClassificationForRtfCopy.OptionName)]
public sealed class UseAccurateClassificationForRtfCopy : EditorOptionDefinition<bool>
{
diff --git a/src/Text/Impl/EditorOptions/EditorOptions.cs b/src/Text/Impl/EditorOptions/EditorOptions.cs
index cb97410..b327faf 100644
--- a/src/Text/Impl/EditorOptions/EditorOptions.cs
+++ b/src/Text/Impl/EditorOptions/EditorOptions.cs
@@ -11,28 +11,30 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
- using System.Linq;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Utilities;
using Microsoft.VisualStudio.Utilities;
internal class EditorOptions : IEditorOptions
{
- IPropertyOwner Scope { get; set; }
+ internal IPropertyOwner Scope { get; private set; }
HybridDictionary OptionsSetLocally { get; set; }
private EditorOptionsFactoryService _factory;
+ internal readonly bool AllowsLateBinding;
FrugalList<WeakReference> DerivedEditorOptions = new FrugalList<WeakReference>();
internal EditorOptions(EditorOptions parent,
IPropertyOwner scope,
- EditorOptionsFactoryService factory)
+ EditorOptionsFactoryService factory,
+ bool allowsLateBinding = false)
{
_parent = parent;
_factory = factory;
this.Scope = scope;
+ this.AllowsLateBinding = allowsLateBinding;
this.OptionsSetLocally = new HybridDictionary();
@@ -250,6 +252,11 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
#endregion
+ internal void SetScope(IPropertyOwner newScope)
+ {
+ this.Scope = newScope;
+ }
+
#region Private Helpers
//A hook so we can tell whether or not the options have been hooked. Used only by unit tests.
diff --git a/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs
index a7f2686..e21ce2b 100644
--- a/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs
+++ b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs
@@ -18,7 +18,8 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
using Microsoft.VisualStudio.Utilities;
[Export(typeof(IEditorOptionsFactoryService))]
- internal sealed class EditorOptionsFactoryService : IEditorOptionsFactoryService
+ [Export(typeof(IEditorOptionsFactoryService2))]
+ internal sealed class EditorOptionsFactoryService : IEditorOptionsFactoryService2
{
[ImportMany(typeof(EditorOptionDefinition))]
internal List<Lazy<EditorOptionDefinition, INameMetadata>> OptionImports { get; set; }
@@ -31,7 +32,6 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
internal GuardedOperations guardedOperations = null;
#region IEditorOptionsFactoryService Members
-
public IEditorOptions GetOptions(IPropertyOwner scope)
{
if (scope == null)
@@ -42,7 +42,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
public IEditorOptions CreateOptions()
{
- return new EditorOptions(this.GlobalOptions as EditorOptions, null, this);
+ return this.CreateOptions(allowsLateBinding: false);
}
public IEditorOptions GlobalOptions
@@ -51,7 +51,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
{
if (_globalOptions == null)
{
- //We're guranteed that the first thing that happens when anyone tries to create options is that the global options will be created first,
+ //We're guaranteed that the first thing that happens when anyone tries to create options is that the global options will be created first,
//so do initialization here.
_globalOptions = new EditorOptions(null, null, this);
@@ -65,6 +65,30 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
}
#endregion
+ #region IEditorOptionsFactoryService2 Members
+ public bool TryBindToScope(IEditorOptions options, IPropertyOwner scope)
+ {
+ if (scope == null)
+ throw new ArgumentNullException(nameof(scope));
+
+ var editorOptions = options as EditorOptions;
+ if ((editorOptions == null) || (!editorOptions.AllowsLateBinding) || scope.Properties.ContainsProperty(typeof(IEditorOptions)))
+ {
+ // options cannot be bound to the specified scope.
+ return false;
+ }
+
+ editorOptions.SetScope(scope);
+ scope.Properties.AddProperty(typeof(IEditorOptions), options);
+ return true;
+ }
+
+ public IEditorOptions CreateOptions(bool allowsLateBinding)
+ {
+ return new EditorOptions(this.GlobalOptions as EditorOptions, null, this, allowsLateBinding: allowsLateBinding);
+ }
+ #endregion
+
private void Initialize()
{
//Don't need to start locking things (yet)
@@ -108,7 +132,7 @@ namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation
}
}
- internal EditorOptionDefinition GetOptionDefinition(string optionId)
+ public EditorOptionDefinition GetOptionDefinition(string optionId)
{
lock (_instantiatedOptionDefinitions)
{
diff --git a/src/Text/Impl/Outlining/OutliningManager.cs b/src/Text/Impl/Outlining/OutliningManager.cs
index 28decbe..6118d72 100644
--- a/src/Text/Impl/Outlining/OutliningManager.cs
+++ b/src/Text/Impl/Outlining/OutliningManager.cs
@@ -398,23 +398,19 @@ namespace Microsoft.VisualStudio.Text.Outlining
foreach (var tagSpan in tagSpans)
{
- var spans = tagSpan.Span.GetSpans(current);
-
- // We only accept this tag if it hasn't been split into multiple spans and if
- // it hasn't had pieces cut out of it from projection. Also, refuse 0-length
- // tags, as they wouldn't be hiding anything.
- if (spans.Count == 1 &&
- spans[0].Length > 0 &&
- spans[0].Length == tagSpan.Span.GetSpans(tagSpan.Span.AnchorBuffer)[0].Length)
+ // Attempt to map this tag up to the top level buffer in a contiguous fashion,
+ // rejecting it if either the start or end fails to map.
+ if (TryContiguousMapToSnapshot(tagSpan, current, out var mappedSpan) &&
+ (mappedSpan.Length > 0))
{
- ITrackingSpan trackingSpan = current.CreateTrackingSpan(spans[0], SpanTrackingMode.EdgeExclusive);
+ ITrackingSpan trackingSpan = current.CreateTrackingSpan(mappedSpan, SpanTrackingMode.EdgeExclusive);
var collapsible = new Collapsible(trackingSpan, tagSpan.Tag);
if (collapsibles.ContainsKey(collapsible))
{
// TODO: Notify providers somehow.
// Or rewrite so that such things are legal.
#if false
- Debug.WriteLine("IGNORING TAG " + spans[0] + " due to span conflict");
+ Debug.WriteLine("IGNORING TAG " + mappedSpan + " due to span conflict");
#endif
}
else
@@ -425,7 +421,7 @@ namespace Microsoft.VisualStudio.Text.Outlining
else
{
#if false
- Debug.WriteLine("IGNORING TAG " + tagSpan.Span.GetSpans(editBuffer) + " because it was split or shortened by projection");
+ Debug.WriteLine("IGNORING TAG " + tagSpan.Span.GetSpans(editBuffer) + " because its start or endpoint failed to map");
#endif
}
}
@@ -433,6 +429,33 @@ namespace Microsoft.VisualStudio.Text.Outlining
return collapsibles;
}
+ /// <summary>
+ /// Alternative to <see cref="IMappingSpan.GetSpans(ITextSnapshot)"/> that maps the span
+ /// as a contiguous unit so that spans are not split by nested projections.
+ /// </summary>
+ /// <remarks>
+ /// Spans fail to contiguously map if one or more of their end points do not exist in that
+ /// snapshot/buffer.
+ /// </remarks>
+ private static bool TryContiguousMapToSnapshot(IMappingTagSpan<IOutliningRegionTag> tagSpan, ITextSnapshot snapshot, out SnapshotSpan span)
+ {
+ var startPoint = tagSpan.Span.Start.GetPoint(snapshot, PositionAffinity.Successor);
+ if (startPoint.HasValue)
+ {
+ var endPoint = tagSpan.Span.End.GetPoint(snapshot, PositionAffinity.Successor);
+ if (endPoint.HasValue)
+ {
+ span = new SnapshotSpan(
+ startPoint.Value,
+ endPoint.Value);
+ return true;
+ }
+ }
+
+ span = default;
+ return false;
+ }
+
private IEnumerable<ICollapsible> MergeRegions(IEnumerable<ICollapsed> currentCollapsed, IEnumerable<ICollapsible> newCollapsibles,
out IEnumerable<ICollapsed> removedRegions)
{
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs b/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs
index 0cf9b54..8fa180f 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionBroker.cs
@@ -40,7 +40,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
_selectionTransformers.Add(_primaryTransformer);
// Ignore normal text structure navigators and take the plain text version to keep ownership of word navigation.
- _textStructureNavigator = Factory.TextStructureNavigatorSelectorService.CreateTextStructureNavigator(_textView.TextViewModel.EditBuffer, Factory.ContentTypeRegistryService.GetContentType("text"));
+ _textStructureNavigator = Factory.TextStructureNavigatorSelectorService.GetTextStructureNavigator(_textView.TextViewModel.EditBuffer);
_textView.LayoutChanged += OnTextViewLayoutChanged;
_textView.Closed += OnTextViewClosed;
@@ -60,9 +60,32 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
private void OnTextViewLayoutChanged(object sender, TextViewLayoutChangedEventArgs e)
{
- if (CurrentSnapshot != e.NewSnapshot)
+ using (var batchOp = BeginBatchOperation())
{
- CurrentSnapshot = e.NewSnapshot;
+ // If we get a text change, we need to go through all the selections and update them to be in the
+ // new snapshot. If there is just a visual change, we could still need to update selections because
+ // word wrap or collapsed regions might have moved around.
+ if (CurrentSnapshot != e.NewSnapshot)
+ {
+ CurrentSnapshot = e.NewSnapshot;
+ }
+ else if (e.NewViewState.VisualSnapshot != e.OldViewState.VisualSnapshot)
+ {
+ // Box selection is special. Moving _boxSelection is easy, but InnerSetBoxSelection will totally
+ // reset all the selections. It's easier to go a different path here than it is to special case
+ // NormalizeSelections, which is also called when adding an individual selection.
+ if (IsBoxSelection)
+ {
+ // MapToSnapshot does take the visual buffer into account as well. Calling it here should do the right thing
+ // for collapsed regions and word wrap.
+ _boxSelection.Selection = _boxSelection.Selection.MapToSnapshot(_currentSnapshot, _textView);
+ InnerSetBoxSelection();
+ }
+ else
+ {
+ NormalizeSelections(true);
+ }
+ }
}
}
@@ -197,19 +220,23 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
- internal void QueueCaretUpdatedEvent(SelectionTransformer selection)
+ internal void QueueCaretUpdatedEvent(SelectionTransformer transformer)
{
// This is set for calls to TransformSelection which doesn't
// make permanent changes to _selectionTransformers.
- if (selection == _standaloneTransformation)
+ if (transformer == _standaloneTransformation)
{
return;
}
- if (selection == _boxSelection)
+ if (transformer == _boxSelection)
{
InnerSetBoxSelection();
}
+ else
+ {
+ transformer.ModifiedByCurrentOperation = true;
+ }
_fireEvents = true;
FireSessionUpdatedIfNotBatched();
@@ -382,6 +409,9 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
private void FireSessionUpdated()
{
+ var changesFromNormalization = NormalizeSelections();
+ _fireEvents = _fireEvents || changesFromNormalization;
+
// Perform merges as late as possible so that each region can act independently for operations.
MergeSelections();
@@ -400,6 +430,42 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
+ /// <summary>
+ /// Takes selections and makes sure they do not occupy space within text elements like collapsed regions or multi-byte
+ /// characters.
+ /// </summary>
+ /// <param name="overrideModifiedFlags">If specified, ignores dirty flags and normalizes everything.</param>
+ /// <returns>True if anything changed, false otherwise.</returns>
+ private bool NormalizeSelections(bool overrideModifiedFlags = false)
+ {
+ bool selectionsChanged = false;
+
+ // Normalizing for box selection is a more drastic action, and needs to be done with perf in mind since it can throw away,
+ // and recreate large numbers of selections.
+ if (_boxSelection == null)
+ {
+ for (int i = 0; i < _selectionTransformers.Count; i++)
+ {
+ if (overrideModifiedFlags || _selectionTransformers[i].ModifiedByCurrentOperation)
+ {
+ _selectionTransformers[i].ModifiedByCurrentOperation = false;
+
+ // Mapping to the current snapshot has the side-affect of moving points away from the middle of text elements,
+ // or in other words collapsed regions and multi-byte characters.
+ var normalizedSelection = _selectionTransformers[i].Selection.MapToSnapshot(_currentSnapshot, _textView);
+
+ if (_selectionTransformers[i].Selection != normalizedSelection)
+ {
+ selectionsChanged = true;
+ _selectionTransformers[i].Selection = normalizedSelection;
+ }
+ }
+ }
+ }
+
+ return selectionsChanged;
+ }
+
private IFeatureService FeatureService
{
get
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs b/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs
index 276dd02..f65fa36 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/MultiSelectionCommandHandler.cs
@@ -2,12 +2,11 @@
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
using Microsoft.VisualStudio.Text.Operations;
+using Microsoft.VisualStudio.Text.Outlining;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
@@ -30,6 +29,9 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
[Import]
internal ITextSearchNavigatorFactoryService TextSearchNavigatorFactoryService { get; set; }
+ [Import]
+ internal IOutliningManagerService OutliningManagerService { get; set; }
+
public string DisplayName => Strings.MultiSelectionCancelCommandName;
public CommandState GetCommandState(EscapeKeyCommandArgs args) => CommandState.Available;
@@ -58,7 +60,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
public CommandState GetCommandState(RemoveLastSecondaryCaretCommandArgs args)
{
var broker = args.TextView.GetMultiSelectionBroker();
- return broker.HasMultipleSelections ? CommandState.Available : CommandState.Unavailable;
+ return broker.HasMultipleSelections || broker.PrimarySelection.Extent.Length > 0 ? CommandState.Available : CommandState.Unavailable;
}
public CommandState GetCommandState(MoveLastCaretDownCommandArgs args)
@@ -113,7 +115,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (result == true)
{
- broker.TryEnsureVisible(newPrimary, EnsureSpanVisibleOptions.AlwaysCenter);
+ broker.TryEnsureVisible(newPrimary, EnsureSpanVisibleOptions.None);
}
return result;
@@ -140,14 +142,14 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (result == true)
{
- broker.TryEnsureVisible(newPrimary, EnsureSpanVisibleOptions.AlwaysCenter);
+ broker.TryEnsureVisible(newPrimary, EnsureSpanVisibleOptions.None);
}
return result;
}
private static Selection InsertDiscoveredMatchRegion(IMultiSelectionBroker broker, Selection primaryRegion, SnapshotSpan found)
- {
+ {
var newSpan = new VirtualSnapshotSpan(found);
var newSelection = new Selection(newSpan, primaryRegion.IsReversed);
broker.AddSelection(newSelection);
@@ -161,6 +163,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (broker.PrimarySelection.IsEmpty)
{
broker.TryPerformActionOnSelection(broker.PrimarySelection, PredefinedSelectionTransformations.SelectCurrentWord, out _);
+ return true;
}
var navigator = TextSearchNavigatorFactoryService.CreateSearchNavigator(args.TextView.TextViewModel.EditBuffer);
@@ -211,8 +214,14 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (!broker.SelectedSpans.OverlapsWith(found))
{
+ var outliningManager = OutliningManagerService.GetOutliningManager(args.TextView);
+ if (outliningManager != null)
+ {
+ outliningManager.ExpandAll(found, collapsible => true);
+ }
+
var addedRegion = InsertDiscoveredMatchRegion(broker, primaryRegion, found);
- broker.TryEnsureVisible(addedRegion, EnsureSpanVisibleOptions.AlwaysCenter);
+ broker.TryEnsureVisible(addedRegion, EnsureSpanVisibleOptions.None);
return true;
}
@@ -249,6 +258,9 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
navigator.Find(); // Get and ignore the primary region
+ var newlySelectedSpans = new List<SnapshotSpan>();
+ var oldSelectedSpans = broker.SelectedSpans;
+
while (navigator.Find())
{
var found = navigator.CurrentResult.Value;
@@ -259,12 +271,45 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
break;
}
- if (!broker.SelectedSpans.OverlapsWith(found))
+ if (!oldSelectedSpans.OverlapsWith(found))
+ {
+ newlySelectedSpans.Add(found);
+ }
+ }
+
+ // Make sure that none of the newly selected spans overlap
+ for(int i = 0; i < (newlySelectedSpans.Count - 1); i++)
+ {
+ if (newlySelectedSpans[i].OverlapsWith(newlySelectedSpans[i+1]))
{
- InsertDiscoveredMatchRegion(broker, primaryRegion, found);
+ newlySelectedSpans.RemoveAt(i + 1);
+
+ // decrement 1 so we can compare i and what used to be i+2 next time
+ i--;
}
}
+ var newlySelectedSpanCollection = new NormalizedSnapshotSpanCollection(newlySelectedSpans);
+
+ // Ok, we've figured out what selections we want to add. Now we need to expand any outlining regions before finally adding the selections
+ var outliningManager = OutliningManagerService.GetOutliningManager(args.TextView);
+ if (outliningManager != null)
+ {
+ var extent = new SnapshotSpan(
+ newlySelectedSpanCollection[0].Start,
+ newlySelectedSpanCollection[newlySelectedSpanCollection.Count - 1].End);
+ outliningManager.ExpandAll(extent, collapsible =>
+ {
+ return newlySelectedSpanCollection.IntersectsWith(collapsible.Extent.GetSpan(broker.CurrentSnapshot));
+ });
+ }
+
+ // Yay, we can finally actually add the selections
+ for (int i = 0; i < newlySelectedSpans.Count; i++)
+ {
+ broker.AddSelectionRange(newlySelectedSpans.Select(span => new Selection(span, broker.PrimarySelection.IsReversed)));
+ }
+
return true;
}
@@ -298,9 +343,13 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
if (broker.TryRemoveSelection(toRemove))
{
- return broker.TryEnsureVisible(FindLastSelection(broker), EnsureSpanVisibleOptions.AlwaysCenter);
+ return broker.TryEnsureVisible(FindLastSelection(broker), EnsureSpanVisibleOptions.None);
}
}
+ else if (broker.PrimarySelection.Extent.Length > 0)
+ {
+ broker.TryPerformActionOnSelection(broker.PrimarySelection, PredefinedSelectionTransformations.ClearSelection, out _);
+ }
return false;
}
@@ -317,7 +366,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
{
if (broker.TryRemoveSelection(toRemove))
{
- return broker.TryEnsureVisible(FindLastSelection(broker), EnsureSpanVisibleOptions.AlwaysCenter);
+ return broker.TryEnsureVisible(FindLastSelection(broker), EnsureSpanVisibleOptions.None);
}
}
}
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs b/src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs
index d4bc06e..e80770c 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/SelectionTransformer.cs
@@ -204,6 +204,15 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
+ /// <summary>
+ /// Determines whether this transformer should be considered as changed by the current operation. This is used
+ /// by normalization code which checks after all operations have completed to ensure that we don't leave a caret
+ /// or selection sitting in the middle of a collapsed region or multi-byte character. It starts as true so that we
+ /// check by default for all new transformers. We need this because the actual normalizing check does expensive formatting
+ /// which we'd like to avoid if possible for selections that were not changed by an operation.
+ /// </summary>
+ internal bool ModifiedByCurrentOperation { get; set; } = true;
+
private static SnapshotPoint GetFirstNonWhiteSpaceCharacterInSpan(SnapshotSpan span)
{
var toReturn = span.Start;
@@ -226,7 +235,7 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
return (endOfWord == endOfLine) && (endOfLine > currentPosition);
}
- private static bool IsSpanABlankLine(SnapshotSpan currentWord, ITextViewLine currentLine)
+ private static bool IsSpanABlankLine(SnapshotSpan currentWord, ITextSnapshotLine currentLine)
{
return currentWord.IsEmpty && currentWord == currentLine.Extent;
}
@@ -236,65 +245,106 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
/// </summary>
/// <param name="previousWord">The current previous word.</param>
/// <param name="line">The line containing previous word.</param>
+ private static bool ShouldContinuePastPreviousWord(SnapshotSpan previousWord, ITextSnapshotLine line)
+ {
+ // If the previous word is whitespace, and the previous word is not the start of the line
+ // then it should be included in the previous word.
+ return char.IsWhiteSpace(previousWord.Snapshot[previousWord.Start]) && previousWord.Start != line.Start;
+ }
+
+ private SnapshotPoint NextCharacter(SnapshotPoint current)
+ {
+ if (current.Snapshot.Length == current.Position)
+ {
+ return current;
+ }
+
+ return _broker.TextView.GetTextElementSpan(current).End;
+ }
- private static bool ShouldContinuePastPreviousWord(SnapshotSpan previousWord, ITextViewLine line)
+ private SnapshotPoint PreviousCharacter(SnapshotPoint current)
{
- return char.IsWhiteSpace(previousWord.Snapshot[previousWord.Start]) && !IsSpanABlankLine(previousWord, line);
+ if (current.Position == 0)
+ {
+ return current;
+ }
+
+ return _broker.TextView.GetTextElementSpan(current - 1).Start;
}
private SnapshotSpan GetNextWord()
{
var currentPosition = _selection.InsertionPoint.Position;
var currentWord = _broker.TextStructureNavigator.GetExtentOfWord(currentPosition);
- var currentLine = _broker.TextView.GetTextViewLineContainingBufferPosition(currentPosition);
+ var currentLine = currentPosition.GetContainingLine();
if (currentWord.Span.End < _currentSnapshot.Length)
{
- // If the current point is at the end of the line, look for the next word on the next line
- if (_selection.InsertionPoint.Position == currentLine.End)
+ // Get the current caret position
+ int startPosition = currentPosition.Position;
+ int endOfLine = currentLine.End.Position;
+
+ if (startPosition >= endOfLine)
{
- var startOfNextLine = currentLine.EndIncludingLineBreak;
- var nextLine = _broker.TextView.GetTextViewLineContainingBufferPosition(startOfNextLine);
- var wordOnNextLine = _broker.TextStructureNavigator.GetExtentOfWord(startOfNextLine);
+ // Move the caret to the next line since it is at the end of the current line
+ currentPosition = currentLine.EndIncludingLineBreak;
+ currentLine = currentPosition.GetContainingLine();
+ endOfLine = currentLine.End.Position;
- if (wordOnNextLine.IsSignificant)
+ // Move past whitespace on the next line
+ while (currentPosition.Position < endOfLine && char.IsWhiteSpace(currentPosition.Snapshot[currentPosition.Position]))
{
- return wordOnNextLine.Span;
- }
- else if (wordOnNextLine.Span.End >= nextLine.End)
- {
- return new SnapshotSpan(nextLine.End, nextLine.End);
+ currentPosition = NextCharacter(currentPosition);
}
- // Simulate continuing a search on the next line recursively by just updating our starting point and falling through.
- currentPosition = startOfNextLine;
- currentWord = wordOnNextLine;
- currentLine = nextLine;
+ return new SnapshotSpan(CurrentSnapshot, currentPosition.Position, 0);
}
- // By default, VS stops at line breaks when determing word
- // boundaries.
- if (ShouldStopAtEndOfLine(currentWord.Span.End, currentLine.End, currentPosition) || IsSpanABlankLine(currentWord.Span, currentLine))
+ currentPosition = NextCharacter(currentPosition);
+
+ // If we are at the end of the line, stop looking for the next word - we want the caret to
+ // stop at the end of each line.
+ if (currentPosition >= endOfLine)
{
- return new SnapshotSpan(currentWord.Span.End, currentWord.Span.End);
+ return new SnapshotSpan(CurrentSnapshot, currentPosition.Position, 0);
}
- var nextWord = _broker.TextStructureNavigator.GetExtentOfWord(currentWord.Span.End);
-
- if (!nextWord.IsSignificant)
+ // Skip past whitespace.
+ while (currentPosition < endOfLine && char.IsWhiteSpace(currentPosition.Snapshot[currentPosition.Position]))
{
- nextWord = _broker.TextStructureNavigator.GetExtentOfWord(nextWord.Span.End);
+ currentPosition = NextCharacter(currentPosition);
}
- int start = nextWord.Span.Start;
- int end = nextWord.Span.End;
+ // If the position is still not at the end of the line get the current
+ // word.
+ if (currentPosition < endOfLine)
+ {
+ currentWord = _broker.TextStructureNavigator.GetExtentOfWord(currentPosition);
- // The text structure navigator can return a word with whitespace attached at the end.
- // Handle that case here.
- start = Math.Max(start, currentWord.Span.End);
- int length = end - start;
+ if (currentWord.Span.Start < currentPosition)
+ {
+ // If the current word starts before the current position, move to the end of the
+ // current word.
+ currentPosition = currentWord.Span.End;
+ }
+
+ bool hitWhitespace = false;
+ // Skip past whitespace at the end of the word.
+ while (currentPosition < endOfLine && char.IsWhiteSpace(currentPosition.Snapshot[currentPosition.Position]))
+ {
+ hitWhitespace = true;
+ currentPosition = NextCharacter(currentPosition);
+ }
- return new SnapshotSpan(_currentSnapshot, start, length);
+ return hitWhitespace ? new SnapshotSpan(CurrentSnapshot, currentPosition, 0) : _broker.TextStructureNavigator.GetExtentOfWord(currentPosition).Span;
+ }
+ else if (currentPosition == endOfLine)
+ {
+ // Just wrap one line be done
+ currentPosition = NextCharacter(currentPosition);
+ }
+
+ return new SnapshotSpan(currentPosition, currentPosition);
}
return new SnapshotSpan(_currentSnapshot, _currentSnapshot.Length, 0);
@@ -304,45 +354,75 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
{
var position = _selection.InsertionPoint.Position;
var word = _broker.TextStructureNavigator.GetExtentOfWord(position);
- var line = _broker.TextView.GetTextViewLineContainingBufferPosition(position);
+ var line = position.GetContainingLine();
- if (word.Span.Start > 0)
+ if (word.Span.Start == 0)
{
- // By default, VS stops at line breaks when determing word
- // boundaries.
- if ((word.Span.Start == line.Start) &&
- (position != line.Start || _selection.InsertionPoint.IsInVirtualSpace))
- {
- return new SnapshotSpan(line.Start, line.Start);
- }
+ // We can't move back anymore, just give the start of the document as an empty span.
+ return new SnapshotSpan(_currentSnapshot, 0, 0);
+ }
+
+ if (word.Span.Start == line.Start && !_selection.InsertionPoint.IsInVirtualSpace)
+ {
+ // We're starting at the beginning of the line, jump to the end of the previous line, then continute the algorithm as normal.
+ var lineNumber = line.LineNumber;
+ line = _broker.CurrentSnapshot.GetLineFromLineNumber(Math.Max(0, lineNumber - 1));
+ position = line.End;
- // If the point is not at the beginning of a word that is not whitespace, it is possible
- // that the "current word" is also the word we wish to navigate to the beginning of.
- if ((word.Span.Start != position) &&
- (!word.Span.IsEmpty))
+ // If the line is empty, just return
+ if (line.Extent.IsEmpty)
{
- return word.Span;
+ return line.Extent;
}
- // Ok, we have to start moving backwards through the document.
- position = word.Span.Start - 1;
+ // Make sure we're not in the middle of a text element like a hidden region, a multi-byte char or something else weird.
+ position = _broker.TextView.GetTextElementSpan(position).Start;
word = _broker.TextStructureNavigator.GetExtentOfWord(position);
- line = _broker.TextView.GetTextViewLineContainingBufferPosition(position);
+ }
- if (word.Span.Start > 0)
- {
- if (ShouldContinuePastPreviousWord(word.Span, line))
- {
- // Move back one more time
- position = word.Span.Start - 1;
- word = _broker.TextStructureNavigator.GetExtentOfWord(position);
- }
- }
+ // By default, VS stops at line breaks when determing word
+ // boundaries.
+ if ((word.Span.Start == line.Start) &&
+ (position != line.Start || _selection.InsertionPoint.IsInVirtualSpace))
+ {
+ return new SnapshotSpan(line.Start, line.Start);
+ }
+ // If the point is not at the beginning of a word that is not whitespace, it is possible
+ // that the "current word" is also the word we wish to navigate to the beginning of.
+ if ((word.Span.Start != position) && (!word.Span.IsEmpty) && word.IsSignificant)
+ {
return word.Span;
}
- return new SnapshotSpan(_currentSnapshot, 0, 0);
+ // Ok, we have to start moving backwards through the document.
+ do
+ {
+ position = PreviousCharacter(position);
+
+ // Skip past whitespace at the start of the word.
+ } while (position > line.Start && char.IsWhiteSpace(position.GetChar()));
+
+ // Again, VS stops at line breaks when determining word boundaries
+ if (position <= line.Start)
+ {
+ return new SnapshotSpan(position, position);
+ }
+
+ word = _broker.TextStructureNavigator.GetExtentOfWord(position);
+ line = position.GetContainingLine();
+
+ if (word.Span.Start > 0)
+ {
+ if (ShouldContinuePastPreviousWord(word.Span, line))
+ {
+ // Move back one more time
+ position = PreviousCharacter(position);
+ word = _broker.TextStructureNavigator.GetExtentOfWord(position);
+ }
+ }
+
+ return word.Span;
}
public void MoveTo(VirtualSnapshotPoint point, bool select, PositionAffinity insertionPointAffinity)
@@ -354,13 +434,52 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
|| (!select && _selection.AnchorPoint != point)
|| _selection.InsertionPointAffinity != insertionPointAffinity)
{
- _selection = new Selection(point, select ? _selection.AnchorPoint : point, point, insertionPointAffinity);
+ var newSelection = new Selection(point, select ? _selection.AnchorPoint : point, point, insertionPointAffinity);
+
+ // Using the ternary here to shortcut out if the snapshots are the same. There's a similar check in the
+ // MapSelectionToCurrentSnapshot method to avoid doing unneeded work, but even spinning up the method call can be expensive.
+ _selection = (newSelection.InsertionPoint.Position.Snapshot == this.CurrentSnapshot)
+ ? newSelection
+ : MapSelectionToCurrentSnapshot(newSelection);
+
_broker.QueueCaretUpdatedEvent(this);
}
}
+ private bool LogException(Exception ex)
+ {
+ _broker.Factory.GuardedOperations.HandleException(this, ex);
+ return true;
+ }
+
+ private Selection MapSelectionToCurrentSnapshot(Selection newSelection)
+ {
+ if (newSelection.InsertionPoint.Position.Snapshot != this.CurrentSnapshot)
+ {
+ try
+ {
+ throw new InvalidOperationException("Selection does not match the current snapshot.");
+ }
+ catch (InvalidOperationException ex) when (LogException(ex))
+ {
+ // This will catch every time, since LogException always returns true.
+
+ // We really should throw here every time, but to limit the number of crashes we see from this immediately
+ // we are doing a best effort here, and only throwing when we can't map forward. Additionally, we're logging
+ // faults with guarded operations in order to identify bad actors so we can fix them before converting this
+ // to the 'throw always' route.
+
+ // This can still throw if mapping doesn't happen, but it should be significantly less often than currently.
+ newSelection = newSelection.MapToSnapshot(this.CurrentSnapshot, _broker.TextView);
+ }
+ }
+
+ return newSelection;
+ }
+
public void MoveTo(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint, VirtualSnapshotPoint insertionPoint, PositionAffinity insertionPointAffinity)
{
+ // See other overload of MoveTo for interesting comments.
this.CheckIsValid();
if (_selection.AnchorPoint != anchorPoint
@@ -368,7 +487,10 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
|| _selection.InsertionPoint != insertionPoint
|| _selection.InsertionPointAffinity != insertionPointAffinity)
{
- _selection = new Selection(insertionPoint, anchorPoint, activePoint, insertionPointAffinity);
+ var newSelection = new Selection(insertionPoint, anchorPoint, activePoint, insertionPointAffinity);
+ _selection = (newSelection.InsertionPoint.Position.Snapshot == this.CurrentSnapshot)
+ ? newSelection
+ : MapSelectionToCurrentSnapshot(newSelection);
_broker.QueueCaretUpdatedEvent(this);
}
}
@@ -469,7 +591,6 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
-
#region Predefined Caret Manipulations
public void ClearSelection()
{
@@ -695,6 +816,13 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
private void SelectCurrentWord()
{
var extent = _broker.TextStructureNavigator.GetExtentOfWord(this.Selection.InsertionPoint.Position);
+
+ // Select word left of caret if the token to the right is just whitespace.
+ if (!extent.IsSignificant && (extent.Span.Start.Position > 0))
+ {
+ extent = _broker.TextStructureNavigator.GetExtentOfWord(this.Selection.InsertionPoint.Position - 1);
+ }
+
var anchor = new VirtualSnapshotPoint(extent.Span.Start);
var active = new VirtualSnapshotPoint(extent.Span.End);
diff --git a/src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs b/src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs
index 1a14405..51f2058 100644
--- a/src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs
+++ b/src/Text/Impl/XPlat/MultiCaretImpl/SelectionUIProperties.cs
@@ -69,25 +69,84 @@ namespace Microsoft.VisualStudio.Text.MultiSelection.Implementation
}
}
- public override ITextViewLine ContainingTextViewLine
+ public override bool TryGetContainingTextViewLine(out ITextViewLine line)
{
- get
+ line = null;
+ if (_broker.TextView.InLayout || _broker.TextView.IsClosed)
+ {
+ return false;
+ }
+
+ // There are cases where people implement ITextView without ITextView2, so I'm doing a best effort here
+ // checking InLayout and IsClosed manually rather than depending on ITextView2.TryGetTextViewLineContainingBufferPosition below.
+ // The try..catch is then just paranoia to avoid throwing from a TryGet* method at all costs.
+ try
{
var bufferPosition = _transformer.Selection.InsertionPoint.Position;
+
+ // Problematic, though it may be, some callers like to check this during a layout, which means our
+ // snapshot isn't always reliable. If this method comes up in a crash, look through the callstack for someone dispatching
+ // a call without looking to see if the view is in the layout.
+
ITextViewLine textLine = _broker.TextView.GetTextViewLineContainingBufferPosition(bufferPosition);
- if ((_transformer.Selection.InsertionPointAffinity == PositionAffinity.Predecessor) && (textLine.Start == bufferPosition) &&
+ line = _broker.TextView.GetTextViewLineContainingBufferPosition(bufferPosition);
+
+ if ((_transformer.Selection.InsertionPointAffinity == PositionAffinity.Predecessor) && (line.Start == bufferPosition) &&
(_broker.TextView.TextSnapshot.GetLineFromPosition(bufferPosition).Start != bufferPosition))
{
//The desired location has precedessor affinity at the start of a word wrapped line, so we
//really want the line before this one.
- textLine = _broker.TextView.GetTextViewLineContainingBufferPosition(bufferPosition - 1);
+ line = _broker.TextView.GetTextViewLineContainingBufferPosition(bufferPosition - 1);
}
- return textLine;
+ return line != null;
+ }
+ catch (Exception ex)
+ {
+ _factory.GuardedOperations.HandleException(this, ex);
+ line = null;
+ return false;
+ }
+ }
+
+ public override ITextViewLine ContainingTextViewLine
+ {
+ get
+ {
+ // Problematic, though it may be, some callers like to check this during a layout, which means our
+ // snapshot isn't always reliable. If this method comes up in a crash, look through the callstack for someone dispatching
+ // a call without looking to see if the view is in the layout.
+ //
+ // The property to check is ITextView.InLayout
+
+ if (TryGetContainingTextViewLine(out var line))
+ {
+ return line;
+ }
+ else
+ {
+ try
+ {
+ throw new InvalidOperationException("Unable to get TextViewLine containing insertion point.");
+ }
+ catch (InvalidOperationException ex) when (LogException(ex))
+ {
+ // This catch block will never be reached because LogException always returns false.
+ return null;
+ }
+ }
}
}
+ private bool LogException(Exception ex)
+ {
+ // Ok, this is weird. What we are doing here is using guarded operations to log errors to ActivityLogs and Telemetry,
+ // but we really have to throw here because by the time you get to this state there's no graceful recovery.
+ _factory.GuardedOperations.HandleException(this, ex);
+ return false;
+ }
+
public double CaretWidth
{
get
diff --git a/src/Text/Util/TextDataUtil/GuardedOperations.cs b/src/Text/Util/TextDataUtil/GuardedOperations.cs
index add89d2..98a6d40 100644
--- a/src/Text/Util/TextDataUtil/GuardedOperations.cs
+++ b/src/Text/Util/TextDataUtil/GuardedOperations.cs
@@ -747,16 +747,32 @@ namespace Microsoft.VisualStudio.Text.Utilities
}).Task;
}
- static bool _ignoreFailures = false;
+ internal static bool IgnoreFailures = false;
+ internal static bool BreakOnFailures = true;
+
+ public bool TryCastToType<TArgs>(object toCast, out TArgs casted)
+ {
+ try
+ {
+ casted = (TArgs)toCast;
+ return true;
+ }
+ catch (Exception ex)
+ {
+ HandleException(this, ex);
+ casted = default(TArgs);
+ return false;
+ }
+ }
[Conditional("DEBUG")]
private static void Fail(string message)
{
- if (!_ignoreFailures)
+ if (!IgnoreFailures)
{
- if (Debugger.IsAttached)
+ if (BreakOnFailures && Debugger.IsAttached)
Debugger.Break();
- Debug.Fail(message);
+ Debug.Fail(message);
}
}
}
diff --git a/src/Text/Util/TextLogicUtil/SideBySideMap.cs b/src/Text/Util/TextLogicUtil/SideBySideMap.cs
new file mode 100644
index 0000000..2ec0faa
--- /dev/null
+++ b/src/Text/Util/TextLogicUtil/SideBySideMap.cs
@@ -0,0 +1,136 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+//
+namespace Microsoft.VisualStudio.Text.Differencing
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using Microsoft.VisualStudio.Text.Utilities;
+
+ public class SideBySideMap
+ {
+ public ISnapshotDifference Difference { get; }
+
+ public int SideBySideLength { get; }
+
+ private IList<int> _sideBySideEndAtDiff;
+
+ /// <summary>
+ /// Create a Side-by-side map for the given snapshot difference. Note that only side-by-side line maps are supported
+ /// (character maps -- needed for word wrap -- will require a different approach since even "matches" could have different
+ /// lengths if ignore white space is turned on).
+ /// </summary>
+ public SideBySideMap(ISnapshotDifference difference)
+ {
+ Requires.NotNull(difference, nameof(difference));
+ this.Difference = difference;
+
+ // Calculate the effective document "length" (where the length of each diff block
+ // is the maximum of the right & left diffs) and store it for each diff.
+ //
+ // This code assumes word wrap is off. If it is on, we need to use the characters in each block.
+ _sideBySideEndAtDiff = new List<int>(difference.LineDifferences.Differences.Count);
+
+ var leftEnd = 0;
+ var rightEnd = 0;
+ var sideBySideEnd = 0;
+ for (int i = 0; (i < difference.LineDifferences.Differences.Count); ++i)
+ {
+ var diff = difference.LineDifferences.Differences[i];
+
+ int newLeftEnd = diff.Left.End;
+ int newRightEnd = diff.Right.End;
+
+ var leftDelta = newLeftEnd - leftEnd;
+ var rightDelta = newRightEnd - rightEnd;
+
+ sideBySideEnd += Math.Max(leftDelta, rightDelta);
+ _sideBySideEndAtDiff.Add(sideBySideEnd);
+
+ leftEnd = newLeftEnd;
+ rightEnd = newRightEnd;
+ }
+
+ // Since we're in a match, (current.RightBufferSnapshot.LineCount - rightEnd) == current.LeftBufferSnapshot.LineCount - leftEnd
+ this.SideBySideLength = sideBySideEnd + difference.RightBufferSnapshot.LineCount - rightEnd;
+ }
+
+ /// <summary>
+ /// Return the buffer position that corresponds to the specified coordinate (which will be either a line number or a character position). The returned position may be
+ /// on either the left or right difference snapshots.
+ /// </summary>
+ public SnapshotPoint BufferPositionFromSideBySideCoordinate(int coordinate)
+ {
+ //Convert the coordinate to a valid line number (0 ... line count - 1).
+ coordinate = Math.Min(Math.Max(0, coordinate), (this.SideBySideLength - 1));
+
+ ListUtilities.BinarySearch(_sideBySideEndAtDiff, (s) => (s - coordinate - 1), out int index);
+
+ ITextSnapshotLine line;
+ if (index >= this.Difference.LineDifferences.Differences.Count)
+ {
+ // We're in a match that follows the last difference (assuming there are any differences).
+ // Count lines backwards from the end of the right buffer.
+ var delta = this.SideBySideLength - coordinate;
+ line = this.Difference.RightBufferSnapshot.GetLineFromLineNumber(this.Difference.RightBufferSnapshot.LineCount - delta);
+ }
+ else
+ {
+ // We either in a difference or in the match that preceeds a difference.
+ // In either case, the sideBySideEnd corresponds to the start of the match.
+ int sideBySideEnd = (index > 0) ? _sideBySideEndAtDiff[index - 1] : 0;
+
+ int delta = coordinate - sideBySideEnd;
+ Span left;
+ Span right;
+ var difference = this.Difference.LineDifferences.Differences[index];
+ left = difference.Left;
+ right = difference.Right;
+ if (difference.Before != null)
+ {
+ left = Span.FromBounds(difference.Before.Left.Start, difference.Left.End);
+ right = Span.FromBounds(difference.Before.Right.Start, difference.Right.End);
+ }
+
+ if (delta < right.Length)
+ {
+ line = this.Difference.RightBufferSnapshot.GetLineFromLineNumber(right.Start + delta);
+ }
+ else
+ {
+ Debug.Assert(delta < left.Length);
+ line = this.Difference.LeftBufferSnapshot.GetLineFromLineNumber(left.Start + delta);
+ }
+ }
+
+ return line.Start;
+ }
+
+ /// <summary>
+ /// Return the coordinate of the given buffer position (which can be on the left or right buffers).
+ /// </summary>
+ public int SideBySideCoordinateFromBufferPosition(SnapshotPoint position)
+ {
+ position = this.Difference.TranslateToSnapshot(position);
+ int lineNumber = position.GetContainingLine().LineNumber;
+ int index = this.Difference.FindMatchOrDifference(position, out Match match, out Difference difference);
+ if (index == 0)
+ {
+ // Either in a leading match or the 1st difference. In either case, the real line number is the side-by-side line number.
+ return lineNumber;
+ }
+
+ difference = this.Difference.LineDifferences.Differences[index - 1];
+ if (position.Snapshot == this.Difference.LeftBufferSnapshot)
+ {
+ return _sideBySideEndAtDiff[index - 1] + (lineNumber - difference.Left.End);
+ }
+ else
+ {
+ return _sideBySideEndAtDiff[index - 1] + (lineNumber - difference.Right.End);
+ }
+ }
+ }
+}
diff --git a/src/Text/Util/TextUIUtil/ExtensionMethods.cs b/src/Text/Util/TextUIUtil/ExtensionMethods.cs
index c791f15..4d93bce 100644
--- a/src/Text/Util/TextUIUtil/ExtensionMethods.cs
+++ b/src/Text/Util/TextUIUtil/ExtensionMethods.cs
@@ -30,8 +30,22 @@ namespace Microsoft.VisualStudio.Text.MultiSelection
var newInsertion = view.NormalizePoint(region.InsertionPoint.TranslateTo(snapshot));
var newActive = view.NormalizePoint(region.ActivePoint.TranslateTo(snapshot));
var newAnchor = view.NormalizePoint(region.AnchorPoint.TranslateTo(snapshot));
+ PositionAffinity positionAffinity;
- return new Selection(newInsertion, newAnchor, newActive, region.InsertionPointAffinity);
+ if (region.Extent.Length == 0)
+ {
+ // Selection is just a caret, respect the caret's prefered affinity.
+ positionAffinity = region.InsertionPointAffinity;
+ }
+ else
+ {
+ // Selection is non-zero length, adjust affinity so that it is always toward the body of the selection.
+ // This attempts to ensure that the caret is always on the same line as the body of the selection in
+ // word wrap scenarios.
+ positionAffinity = newAnchor < newActive ? PositionAffinity.Predecessor : PositionAffinity.Successor;
+ }
+
+ return new Selection(newInsertion, newAnchor, newActive, positionAffinity);
}
/// <summary>