From 30e88b1f2ce3ee9897e1f3b87c80c64292c27236 Mon Sep 17 00:00:00 2001 From: Kirill Osenkov Date: Wed, 5 Dec 2018 15:09:58 -0800 Subject: Update to VS-Platform 16.0.142-g25b7188c54. 25b7188c54f0cdf6a5be87eeb38e3c6046d5788e --- .../TextLogic/IEditorOptionsFactoryService2.cs | 40 ++ .../Internal/TextLogic/ILoggingServiceInternal.cs | 11 +- .../Def/Internal/TextLogic/IsOptionAttribute.cs | 20 + .../Def/Internal/TextLogic/IsRoamingAttribute.cs | 20 + src/Text/Def/Internal/TextLogic/ParentAttribute.cs | 20 + .../Def/Internal/TextLogic/TelemetrySeverity.cs | 22 + .../Def/TextLogic/EditorOptions/DefaultOptions.cs | 142 +++++ .../DifferenceViewer/DifferenceViewerOptions.cs | 7 + .../TextUI/DifferenceViewer/IDifferenceViewer2.cs | 16 + src/Text/Def/TextUI/Editor/ITextView2.cs | 51 +- src/Text/Def/TextUI/Editor/TextViewExtensions.cs | 71 ++- src/Text/Def/TextUI/EditorOptions/ViewOptions.cs | 28 +- .../AbstractSelectionPresentationProperties.cs | 8 + .../Utilities/AbstractUIThreadOperationContext.cs | 100 +++- src/Text/Def/TextUI/Utilities/IStatusBarService.cs | 22 + .../TextUI/Utilities/IUIThreadOperationExecutor.cs | 22 +- .../IUIThreadOperationTimeoutController.cs | 44 ++ .../Utilities/UIThreadOperationExecutionOptions.cs | 64 ++ .../Impl/Commanding/CommandingStrings.Designer.cs | 11 +- src/Text/Impl/Commanding/CommandingStrings.resx | 3 + .../Impl/Commanding/EditorCommandHandlerService.cs | 211 +++++-- .../EditorCommandHandlerServiceFactory.cs | 45 +- .../Commanding/EditorCommandHandlerServiceState.cs | 55 ++ src/Text/Impl/EditorOperations/EditorOperations.cs | 653 ++++++++++++++++----- src/Text/Impl/EditorOptions/EditorOptions.cs | 13 +- .../EditorOptions/EditorOptionsFactoryService.cs | 34 +- src/Text/Impl/Outlining/OutliningManager.cs | 45 +- .../XPlat/MultiCaretImpl/MultiSelectionBroker.cs | 78 ++- .../MultiCaretImpl/MultiSelectionCommandHandler.cs | 71 ++- .../XPlat/MultiCaretImpl/SelectionTransformer.cs | 256 ++++++-- .../XPlat/MultiCaretImpl/SelectionUIProperties.cs | 69 ++- src/Text/Util/TextDataUtil/GuardedOperations.cs | 24 +- src/Text/Util/TextLogicUtil/SideBySideMap.cs | 136 +++++ src/Text/Util/TextUIUtil/ExtensionMethods.cs | 16 +- 34 files changed, 2083 insertions(+), 345 deletions(-) create mode 100644 src/Text/Def/Internal/TextLogic/IEditorOptionsFactoryService2.cs create mode 100644 src/Text/Def/Internal/TextLogic/IsOptionAttribute.cs create mode 100644 src/Text/Def/Internal/TextLogic/IsRoamingAttribute.cs create mode 100644 src/Text/Def/Internal/TextLogic/ParentAttribute.cs create mode 100644 src/Text/Def/Internal/TextLogic/TelemetrySeverity.cs create mode 100644 src/Text/Def/TextUI/DifferenceViewer/IDifferenceViewer2.cs create mode 100644 src/Text/Def/TextUI/Utilities/IStatusBarService.cs create mode 100644 src/Text/Def/TextUI/Utilities/IUIThreadOperationTimeoutController.cs create mode 100644 src/Text/Def/TextUI/Utilities/UIThreadOperationExecutionOptions.cs create mode 100644 src/Text/Impl/Commanding/EditorCommandHandlerServiceState.cs create mode 100644 src/Text/Util/TextLogicUtil/SideBySideMap.cs (limited to 'src/Text') 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 +{ + /// + /// 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). + /// + public interface IEditorOptionsFactoryService2 : IEditorOptionsFactoryService + { + /// + /// Create a new that is not bound to a particular scope. + /// + /// If true, this option can be bound to a scope after it has been created using . + /// + IEditorOptions CreateOptions(bool allowLateBinding); + + + /// + /// Binds to the specified scope if the scope does not have pre-existing and was + /// created using with the late binding allowed. + /// + /// true if was bound to . + bool TryBindToScope(IEditorOptions option, IPropertyOwner scope); + + /// + /// Get the option definition associated with . + /// + /// + /// + 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. /// + /// TelemetryEventCorrelations which help correlate this fault to the scope it was executing within void PostFault( string eventName, string description, Exception exceptionObject, - string additionalErrorInfo, - bool? isIncludedInWatsonSample); + string additionalErrorInfo = null, + bool? isIncludedInWatsonSample = null, + object[] correlations = null + ); /// /// Adjust the counter associated with and by . @@ -85,5 +88,9 @@ namespace Microsoft.VisualStudio.Text.Utilities /// The counters are cleared as a side-effect of this call. /// void PostCounters(); + + object CreateTelemetryOperationEventScope(string eventName, TelemetrySeverity severity, object[] correlations, IDictionary 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 +{ + /// + /// Attribute defining whether or not the description has an associated option. + /// + /// + /// Defaults to true. Set to false for static elements in the options page. + /// + 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 +{ + /// + /// Attribute defining whether or not the option roams. + /// + /// + /// If not provided, then the option is not considered a roaming attribute. + /// + 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 +{ + /// + /// Attribute indicating the parent option/page of the option. + /// + /// + /// Options can be nested or attached directly to the their corresponding page. + /// + 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 TooltipAppearanceCategoryOptionId = new EditorOptionKey(TooltipAppearanceCategoryOptionName); public const string TooltipAppearanceCategoryOptionName = "TooltipAppearanceCategory"; + /// + /// The default option that determines whether files, when opened, attempt to detect for a utf-8 encoding. + /// + public static readonly EditorOptionKey AutoDetectUtf8Id = new EditorOptionKey(AutoDetectUtf8Name); + public const string AutoDetectUtf8Name = "AutoDetectUtf8"; + + /// + /// The default option that determines whether matching delimiters should be highlighted. + /// + public static readonly EditorOptionKey AutomaticDelimiterHighlightingId = new EditorOptionKey(AutomaticDelimiterHighlightingName); + public const string AutomaticDelimiterHighlightingName = "AutomaticDelimiterHighlighting"; + + /// + /// The default option that determines whether files should follow project coding conventions. + /// + public static readonly EditorOptionKey FollowCodingConventionsId = new EditorOptionKey(FollowCodingConventionsName); + public const string FollowCodingConventionsName = "FollowCodingConventions"; + + /// + /// The default option that determines the editor emulation mode. + /// + public static readonly EditorOptionKey EditorEmulationModeId = new EditorOptionKey(EditorEmulationModeName); + public const string EditorEmulationModeName = "EditorEmulationMode"; + + /// + /// The option definition that determines maximum allowed typing latency value in milliseconds. Its value comes either + /// from remote settings or from if user specifies it in + /// Tools/Options/Text Editor/Advanced page. + /// + internal static readonly EditorOptionKey MaximumTypingLatencyOptionId = new EditorOptionKey(MaximumTypingLatencyOptionName); + internal const string MaximumTypingLatencyOptionName = "MaximumTypingLatency"; + + /// + /// 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 option. + /// + internal static readonly EditorOptionKey UserCustomMaximumTypingLatencyOptionId = new EditorOptionKey(UserCustomMaximumTypingLatencyOptionName); + internal const string UserCustomMaximumTypingLatencyOptionName = "UserCustomMaximumTypingLatency"; + + /// + /// The option definition that determines whether to enable typing latency guarding. + /// + internal static readonly EditorOptionKey EnableTypingLatencyGuardOptionId = new EditorOptionKey(EnableTypingLatencyGuardOptionName); + internal const string EnableTypingLatencyGuardOptionName = "EnableTypingLatencyGuard"; #endregion } @@ -408,5 +453,102 @@ namespace Microsoft.VisualStudio.Text.Editor public override EditorOptionKey Key { get { return DefaultOptions.TooltipAppearanceCategoryOptionId; } } } + /// + /// The option definition that determines whether files, when opened, attempt to detect for a utf-8 encoding. + /// + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.AutoDetectUtf8Name)] + public sealed class AutoDetectUtf8Option : EditorOptionDefinition + { + public override bool Default { get => true; } + + public override EditorOptionKey Key { get { return DefaultOptions.AutoDetectUtf8Id; } } + } + + /// + /// The option definition that determines whether matching delimiters should be highlighted. + /// + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.AutomaticDelimiterHighlightingName)] + public sealed class AutomaticDelimiterHighlightingOption : EditorOptionDefinition + { + public override bool Default { get => true; } + + public override EditorOptionKey Key { get { return DefaultOptions.AutomaticDelimiterHighlightingId; } } + } + + /// + /// The option definition that determines whether files should follow project coding conventions. + /// + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.FollowCodingConventionsName)] + public sealed class FollowCodingConventionsOption : EditorOptionDefinition + { + public override bool Default { get => true; } + + public override EditorOptionKey Key { get { return DefaultOptions.FollowCodingConventionsId; } } + } + + /// + /// The option definition that determines the editor emulation mode. + /// + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.EditorEmulationModeName)] + public sealed class EditorEmulationModeOption : EditorOptionDefinition + { + public override int Default { get => 0; } + + public override EditorOptionKey Key { get { return DefaultOptions.EditorEmulationModeId; } } + } + + /// + ///The option definition that determines whether to enable typing latency guarding. + /// + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.EnableTypingLatencyGuardOptionName)] + internal sealed class EnableTypingLatencyGuard : EditorOptionDefinition + { + /// + /// Gets the default value (true). + /// + public override bool Default { get => true; } + + /// + /// Gets the editor option key. + /// + public override EditorOptionKey Key { get { return DefaultOptions.EnableTypingLatencyGuardOptionId; } } + } + + /// + /// The option definition that determines maximum allowed typing latency value in milliseconds. Its value comes either + /// from remote settings or from if user specifies it in + /// Tools/Options/Text Editor/Advanced page. + /// + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.MaximumTypingLatencyOptionName)] + internal sealed class MaximumTypingLatency : EditorOptionDefinition + { + /// + /// Gets the default value (infinite). + /// + public override int Default { get => Timeout.Infinite; } + + /// + /// Gets the editor option key. + /// + public override EditorOptionKey Key { get { return DefaultOptions.MaximumTypingLatencyOptionId; } } + } + + /// + /// 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 option. + /// + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.UserCustomMaximumTypingLatencyOptionName)] + internal sealed class UserCustomMaximumTypingLatencyOption : EditorOptionDefinition + { + public override int Default { get { return Timeout.Infinite; } } + public override EditorOptionKey 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 /// This option is ignored in the other view modes. public static readonly EditorOptionKey SynchronizeSideBySideViewsId = new EditorOptionKey(DifferenceViewerOptions.SynchronizeSideBySideViewsName); public const string SynchronizeSideBySideViewsName = "Diff/View/SynchronizeSideBySideViews"; + + + /// + /// If true, show the difference overview margin. + /// + public static readonly EditorOptionKey ShowDiffOverviewMarginId = new EditorOptionKey(DifferenceViewerOptions.ShowDiffOverviewMarginName); + public const string ShowDiffOverviewMarginName = "Diff/View/ShowDiffOverviewMargin"; } /// 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 + { + /// + /// Raised when the difference viewer is fully initialized. + /// + 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; } - /// /// Raised whenever the view's MaxTextRightCoordinate is changed. /// @@ -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). /// event EventHandler MaxTextRightCoordinateChanged; + + /// + /// 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. + /// + /// The action to be performed. + void QueuePostLayoutAction(Action action); + + /// + /// Attempts to get a read-only list of the objects rendered in this view. + /// + /// + /// This list will be dense. That is, all characters between the first character of the first through + /// the last character of the last will be represented in one of the objects, + /// except when the layout of the objects is in progress. + /// + /// objects are disjoint. That is, a given character is part of only one . + /// + /// + /// The objects are sorted by the index of their first character. + /// + /// Some of the objects may not be visible, + /// and all objects will be disposed of when the view + /// recomputes its layout. + /// This list is occasionally not available due to layouts or other events, and callers should be prepared to handle + /// a failure. + /// + /// Returns out the requested. + /// True if succeeded, false otherwise. + bool TryGetTextViewLines(out ITextViewLineCollection textViewLines); + + /// + /// Attempts to get the that contains the specified text buffer position. + /// + /// + /// The text buffer position used to search for a text line. + /// + /// + /// True if succeeded, false otherwise. + /// + /// + /// This method returns an if it exists in the view. + /// If the line does not exist in the cache of formatted lines, it will be formatted and added to the cache. + /// The returned could be invalidated by either a layout by the view or by subsequent calls to this method. + /// It is occasionally invalid to retrieve an due to layouts or other events. Callers should be prepared to handle + /// a failure. + /// + /// Returns out the requested. + 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; + } + + /// + /// See . + /// + public static void QueuePostLayoutAction(this ITextView textView, Action action) + { + if (textView == null) + { + throw new ArgumentNullException(nameof(textView)); + } + + (textView as ITextView2)?.QueuePostLayoutAction(action); + } + + /// + /// See . + /// + 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; + } + } + + /// + /// See . + /// + 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 ErrorMarginWidthOptionId = new EditorOptionKey(ErrorMarginWidthOptionName); + /// + /// Determines whether to have a file health indicator. + /// + public static readonly EditorOptionKey EnableFileHealthIndicatorOptionId = new EditorOptionKey(EnableFileHealthIndicatorOptionName); + public const string EnableFileHealthIndicatorOptionName = "TextViewHost/FileHealthIndicator"; + #endregion } @@ -658,7 +664,7 @@ namespace Microsoft.VisualStudio.Text.Editor /// /// Gets the default value, which is WordWrapStyles.None. /// - public override WordWrapStyles Default { get { return WordWrapStyles.None; } } + public override WordWrapStyles Default { get { return WordWrapStyles.AutoIndent; } } /// /// Gets the default text view host value. @@ -893,7 +899,7 @@ namespace Microsoft.VisualStudio.Text.Editor /// /// Gets the default value, which is false. /// - public override bool Default { get { return false; } } + public override bool Default { get { return true; } } /// /// Gets the default text view host value. @@ -1002,4 +1008,22 @@ namespace Microsoft.VisualStudio.Text.Editor /// public override EditorOptionKey Key => DefaultTextViewOptions.CaretWidthId; } + + /// + /// Defines the option to enable the File Health Indicator. + /// + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultTextViewHostOptions.EnableFileHealthIndicatorOptionName)] + public sealed class FileHealthIndicatorEnabled : ViewOptionDefinition + { + /// + /// Gets the default value, which is true. + /// + public override bool Default { get { return true; } } + + /// + /// Gets the default text view host value. + /// + public override EditorOptionKey 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 that contains the . /// public virtual ITextViewLine ContainingTextViewLine { get; } + + /// + /// Tries to get the that contains the . + /// This can fail if the call happens during a view layout or after the view is closed. + /// + /// Returns out the requested line if available, or null otherwise. + /// True if successful, false otherwise. + 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 _scopes; + private ImmutableList _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.Empty; } /// @@ -56,12 +58,14 @@ namespace Microsoft.VisualStudio.Utilities return false; } - if (_scopes == null || _scopes.Count == 0) + ImmutableList 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 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 LazyScopes => _scopes ?? (_scopes = new List()); - /// /// Gets current list of s in this context. /// - public virtual IEnumerable Scopes => this.LazyScopes; + public virtual IEnumerable Scopes => _scopes; /// /// A collection of properties. /// - 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; + } + } /// /// 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 oldScopes = _scopes; + ImmutableList newScopes = oldScopes == null ? ImmutableList.Create(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 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 oldScopes = _scopes; + ImmutableList 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 { private bool _allowCancellation; private string _description; - private IProgress _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 Progress => _progress ?? (_progress = new Progress((progressInfo) => OnProgressChanged(progressInfo))); + public IProgress 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.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 +{ + /// + /// A status bar service enabling to send messages to the editor host's status bar. + /// + /// + /// This is a MEF component part, and should be imported as follows: + /// [Import] + /// IStatusBarService statusBarService = null; + /// + /// + internal interface IStatusBarService + { + /// + /// Sends a text to the editor host's status bar. + /// + /// A text to be displayed on the status bar. + 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 /// /// Executes the action synchronously and waits for it to complete. /// - /// Operation's title. + /// Operation's title. Can be null to indicate that the wait dialog should use the application's title. /// Default operation's description, which is displayed on the wait dialog unless /// one or more s with more specific descriptions were added to /// the . @@ -75,11 +75,19 @@ namespace Microsoft.VisualStudio.Utilities UIThreadOperationStatus Execute(string title, string defaultDescription, bool allowCancellation, bool showProgress, Action action); + /// + /// Executes the action synchronously and waits for it to complete. + /// + /// Options that control action execution behavior. + /// An action to execute. + /// A status of action execution. + UIThreadOperationStatus Execute(UIThreadOperationExecutionOptions executionOptions, Action action); + /// /// Begins executing potentially long running operation on the caller thread and provides a context object that provides access to shared /// cancellability and wait indication. /// - /// Operation's title. + /// Operation's title. Can be null to indicate that the wait dialog should use the application's title. /// Default operation's description, which is displayed on the wait dialog unless /// one or more s with more specific descriptions were added to /// the . @@ -89,5 +97,15 @@ namespace Microsoft.VisualStudio.Utilities /// cancellability and wait indication for the given operation. The operation is considered executed /// when this instance is disposed. IUIThreadOperationContext BeginExecute(string title, string defaultDescription, bool allowCancellation, bool showProgress); + + /// + /// Begins executing potentially long running operation on the caller thread and provides a context object that provides access to shared + /// cancellability and wait indication. + /// + /// Options that control execution behavior. + /// instance that provides access to shared two way + /// cancellability and wait indication for the given operation. The operation is considered executed + /// when this instance is disposed. + 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 +{ + /// + /// A controller that enables and controls auto-cancellation of an operation execution by + /// on a timeout. + /// + public interface IUIThreadOperationTimeoutController + { + /// + /// The duration (in milliseconds) after which an operation shouold be auto-cancelled. + /// + /// disables auto-cancellation. + int CancelAfter { get; } + + /// + /// Gets whether an operation, whose execution time exceeded timeout should be + /// cancelled. + /// + /// 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. + bool ShouldCancel(); + + /// + /// An event callback raised when an operation execution timeout was reached. + /// + /// Indicates whether an operation was auto-cancelled. + /// Might be false if the operation is not cancellable ( + /// is false or returned false. + /// 7 + /// This method is called on a background thread. + void OnTimeout(bool wasExecutionCancelled); + + /// + /// An event callback raised when a UI thread operation execution took long enough to be considered + /// as a delay. Visual Studio implementation of the displays + /// a wait dialog at this point. + /// + /// This method is called on a background thread. + 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 +{ + /// + /// Options that control behavior of . + /// + public class UIThreadOperationExecutionOptions + { + /// + /// Operation's title. + /// + public string Title { get; } + + /// + /// Default operation's description, which is displayed on the wait dialog unless + /// one or more s with more specific descriptions were added to + /// the . + /// + public string DefaultDescription { get; } + + /// + /// Whether to allow cancellability. + /// + public bool AllowCancellation { get; } + + /// + /// Whether to show progress indication. + /// + public bool ShowProgress { get; } + + /// + /// A controller that enables and controls auto-cancellation of an operation execution on a timeout. + /// + public IUIThreadOperationTimeoutController TimeoutController { get; } + + /// + /// Creates a new instance of the . + /// + /// Operation's title. Can be null to indicate that the wait dialog should use the application's title. + /// Default operation's description, which is displayed on the wait dialog unless + /// one or more s with more specific descriptions were added to + /// the . + /// Whether to allow cancellability. + /// Whether to show progress indication. + /// A controller that enables and controls auto-cancellation of an operation execution on a timeout. + 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 { @@ -60,6 +60,15 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation { } } + /// + /// Looks up a localized string similar to Command handler '{0}' has exceeded allotted timeout and was auto canceled.. + /// + internal static string CommandCancelled { + get { + return ResourceManager.GetString("CommandCancelled", resourceCulture); + } + } + /// /// Looks up a localized string similar to Please wait for an editor command to finish.... /// 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 @@ Please wait for an editor command to finish... + + Command handler '{0}' has exceeded allotted timeout and was auto canceled. + \ 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; -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 _commandHandlers; - private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor; - private readonly JoinableTaskContext _joinableTaskContext; + private readonly EditorCommandHandlerServiceFactory _factory; private readonly ITextView _textView; - private readonly IComparer> _contentTypesComparer; private readonly ICommandingTextBufferResolver _bufferResolver; - private readonly IGuardedOperations _guardedOperations; private readonly static IReadOnlyList EmptyHandlerList = new List(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> _commandHandlersByTypeAndContentType; - public EditorCommandHandlerService(ITextView textView, + public EditorCommandHandlerService(EditorCommandHandlerServiceFactory factory, + ITextView textView, IEnumerable commandHandlers, - IUIThreadOperationExecutor uiThreadOperationExecutor, JoinableTaskContext joinableTaskContext, - IComparer> 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>(); _bufferResolver = bufferResolver ?? throw new ArgumentNullException(nameof(bufferResolver)); - _guardedOperations = guardedOperations ?? throw new ArgumentNullException(nameof(guardedOperations)); } public CommandState GetCommandState(Func argsFactory, Func 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(Func 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 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 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() 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 newCommandHandlerList = null; foreach (var lazyCommandHandler in SelectMatchingCommandHandlers(_commandHandlers, contentType, textViewRoles)) { - var commandHandler = _guardedOperations.InstantiateExtension(this, lazyCommandHandler); + var commandHandler = _factory.GuardedOperations.InstantiateExtension(this, lazyCommandHandler); if (commandHandler is ICommandHandler || commandHandler is IChainedCommandHandler) { 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> _commandHandlers; private readonly IList> _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> 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> OrderCommandHandlers(IEnumerable> 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 _executingCommandHandlers = new ConcurrentStack(); + + 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(); + 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 /// private const string _boxSelectionCutCopyTag = "MSDEVColumnSelect"; -#endregion // Private Members + #endregion // Private Members /// /// Constructs an bound to a given . @@ -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 /// 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 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; + }); } /// @@ -1661,58 +1877,158 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation /// public bool Unindent() { - bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box && - _textView.Selection.Start != _textView.Selection.End; + if (UnindentWillCreateEdit()) + { + Func action = () => + { + using (_multiSelectionBroker.BeginBatchOperation()) + { + var succeeded = this.EditHelper(edit => + { + if (_multiSelectionBroker.IsBoxSelection) + { + return UnindentBoxSelection(edit); + } + else + { + return UnindentStreamSelections(edit); + } + }); - Func 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 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 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 action) + private bool PerformIndentActionOnEachBufferLine(Func action) + { + Func 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 action) { - Func editAction = edit => + Func 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?); } /// @@ -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); - } - /// /// Used by the un-indenting logic to determine what an unindent means in virtual space. /// @@ -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 /// /// 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 editAction) { @@ -4506,7 +4862,7 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation }); } -#endregion + #endregion internal bool IsEmptyBoxSelection() { @@ -4892,6 +5248,21 @@ namespace Microsoft.VisualStudio.Text.Operations.Implementation public override EditorOptionKey Key { get { return MaxRtfCopyLength.OptionKey; } } } + [Export(typeof(EditorOptionDefinition))] + [Name(EnableRtfCopy.OptionName)] + public sealed class EnableRtfCopy : EditorOptionDefinition + { + public const string OptionName = "EnableRtfCopy"; + public static readonly EditorOptionKey OptionKey = new EditorOptionKey(EnableRtfCopy.OptionName); + + public override bool Default { get { return true; } } + + /// + /// Gets the editor option key. + /// + public override EditorOptionKey Key { get { return EnableRtfCopy.OptionKey; } } + } + [Export(typeof(EditorOptionDefinition))] [Name(UseAccurateClassificationForRtfCopy.OptionName)] public sealed class UseAccurateClassificationForRtfCopy : EditorOptionDefinition 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 DerivedEditorOptions = new FrugalList(); 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> 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; } + /// + /// Alternative to that maps the span + /// as a contiguous unit so that spans are not split by nested projections. + /// + /// + /// Spans fail to contiguously map if one or more of their end points do not exist in that + /// snapshot/buffer. + /// + private static bool TryContiguousMapToSnapshot(IMappingTagSpan 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 MergeRegions(IEnumerable currentCollapsed, IEnumerable newCollapsibles, out IEnumerable 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 } } + /// + /// Takes selections and makes sure they do not occupy space within text elements like collapsed regions or multi-byte + /// characters. + /// + /// If specified, ignores dirty flags and normalizes everything. + /// True if anything changed, false otherwise. + 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(); + 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 } } + /// + /// 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. + /// + 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 /// /// The current previous word. /// The line containing previous word. + 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(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 _sideBySideEndAtDiff; + + /// + /// 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). + /// + 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(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; + } + + /// + /// 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. + /// + 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; + } + + /// + /// Return the coordinate of the given buffer position (which can be on the left or right buffers). + /// + 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); } /// -- cgit v1.2.3