diff options
author | Sandy Armstrong <sandy@xamarin.com> | 2019-10-28 18:14:29 +0300 |
---|---|---|
committer | Sandy Armstrong <sandy@xamarin.com> | 2019-10-28 18:14:29 +0300 |
commit | 0306a5f676a0fbf184328aa932abe37a4923af17 (patch) | |
tree | 3f3e1612d463f0b9034a36aba568433be2cbef70 | |
parent | dc60fc684aaa490dcc4e4d768704bbfe81aa858c (diff) |
Sync with vs-editor-core@140e98831
15 files changed, 988 insertions, 53 deletions
diff --git a/src/Editor/Text/Def/TextData/AssemblyInfo.cs b/src/Editor/Text/Def/TextData/AssemblyInfo.cs index d70d069..f195c9b 100644 --- a/src/Editor/Text/Def/TextData/AssemblyInfo.cs +++ b/src/Editor/Text/Def/TextData/AssemblyInfo.cs @@ -16,6 +16,26 @@ using System.Security.Permissions; [assembly: ComponentGuarantees(ComponentGuaranteesOptions.Stable)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Editor.UnitTests, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Platform.VSEditor, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Internal, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Data.Utilities, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Editor.IntegrationTests, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Model.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Model.Implementation.UnitTests, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.TextViewUnitTestHelper, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.UI, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.UnitTestHelper, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.UI.Text.EditorOperations.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.IndentationManager.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.UI.Text.Wpf.View.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Logic.Text.Find.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Internal.UnitTests, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Text.Outlining.Implementation, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.Logic.Text.Outlining.Implementation.UnitTests, PublicKey=" + ThisAssembly.PublicKey)] +[assembly: InternalsVisibleTo("Microsoft.VisualStudio.UI.Text.EditorOperations.UnitTests, PublicKey=" + ThisAssembly.PublicKey)] [assembly: AssemblyTrademark ("")] [assembly: AssemblyCulture ("")] diff --git a/src/Editor/Text/Def/TextData/Document/IWhitespaceManager.cs b/src/Editor/Text/Def/TextData/Document/IWhitespaceManager.cs new file mode 100644 index 0000000..ec590f2 --- /dev/null +++ b/src/Editor/Text/Def/TextData/Document/IWhitespaceManager.cs @@ -0,0 +1,22 @@ +namespace Microsoft.VisualStudio.Text.Document +{ + /// <summary> + /// Subscribes to buffer change events and provides access to a <see cref="NewlineState"/> + /// object and a <see cref="LeadingWhitespaceState"/> that are kept in sync with the state + /// of the buffer provided at creation time. + /// </summary> + internal interface IWhitespaceManager + { + /// <summary> + /// Gets an instance of <see cref="NewlineState"/> that is kept in sync with the buffer + /// provided at creation time. + /// </summary> + NewlineState NewlineState { get; } + + /// <summary> + /// Gets an instance of <see cref="LeadingWhitespaceState"/> that is kept in sync with the + /// buffer provided at creation time. + /// </summary> + LeadingWhitespaceState LeadingWhitespaceState { get; } + } +} diff --git a/src/Editor/Text/Def/TextData/Document/IWhitespaceManagerFactory.cs b/src/Editor/Text/Def/TextData/Document/IWhitespaceManagerFactory.cs new file mode 100644 index 0000000..62923a9 --- /dev/null +++ b/src/Editor/Text/Def/TextData/Document/IWhitespaceManagerFactory.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Text.Document +{ + /// <summary> + /// Creates Whitespace Managers + /// </summary> + /// <remarks> + /// This is a MEF component part, and should be imported as follows: + /// [Import] + /// IEditingStateFactory factory = null; + /// </remarks> + interface IWhitespaceManagerFactory + { + /// <summary> + /// Gets or creates an instance if <see cref="IWhitespaceManager"/> from the provided parameters. + /// There will be at most one manager created per buffer. Subsequent calls will use the existing + /// newline state, and that parameter will be ignored. + /// </summary> + /// <param name="buffer">The buffer associated with the whitespace manager.</param> + /// <param name="newlineState"> + /// A seed newline state that can be pre-filled with counts of the newlines in a document. The buffer + /// will be tracked forward through edits to update the newline state, so accurate starting values are critical. + /// </param> + /// <returns>A whitespace manager that has been subscribed to track edits in the given buffer.</returns> + IWhitespaceManager GetOrCreateWhitespaceManager( + ITextBuffer buffer, + NewlineState initialNewlineState, + LeadingWhitespaceState initialLeadingWhitespaceState); + + /// <summary> + /// Tries to get a whitespace manager that already exists for the given buffer. + /// </summary> + /// <param name="buffer">The buffer on which to search for an associated whitespace manager.</param> + /// <param name="manager">The instance of the manager if successfully found.</param> + /// <returns>True if successfully found a manager, false otherwise.</returns> + bool TryGetExistingWhitespaceManager(ITextBuffer buffer, out IWhitespaceManager manager); + } +} diff --git a/src/Editor/Text/Def/TextData/Document/LeadingWhitespaceState.cs b/src/Editor/Text/Def/TextData/Document/LeadingWhitespaceState.cs new file mode 100644 index 0000000..51f7661 --- /dev/null +++ b/src/Editor/Text/Def/TextData/Document/LeadingWhitespaceState.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// LeadingWhitespaceState contains counts of lines starting with space, tab, or neither. It can perform basic logic based on + /// those counts. One of these is created for each open document, and is kept up-to-date by watching edits through the + /// lifetime of the document. + /// </summary> + internal class LeadingWhitespaceState + { + /// <summary> + /// Describes the supported types of leading characters. + /// </summary> + public enum LineLeadingCharacter + { + Tab, + Space, + Printable, + Empty + } + + public int LinesBeginningWithSpaces => _space; + public int LinesBeginningWithTabs => _tab; + + // Counts for the various kinds of leading characters. Internal for testing + internal int _tab; + internal int _space; + internal int _printable; + internal int _empty; + + /// <summary> + /// Increments the count for a leading character by the provided number + /// </summary> + /// <param name="leadingCharacter">Leading character to be adjusted</param> + /// <param name="count">May be any positive or negative value</param> + public void Increment(LineLeadingCharacter leadingCharacter, int count) + { + switch (leadingCharacter) + { + case LineLeadingCharacter.Tab: + _tab += count; + break; + case LineLeadingCharacter.Space: + _space += count; + break; + case LineLeadingCharacter.Printable: + _printable += count; + break; + case LineLeadingCharacter.Empty: + _empty += count; + break; + default: + throw new ArgumentException($"Unknown leading whitespace value {leadingCharacter}", nameof(leadingCharacter)); + } + } + + /// <summary> + /// Gets whether the leading WHITESPACE characters are in a consistent state, + /// (either 0 or 1 kind of leading whites[ace in the document). Empty lines and + /// lines starting with printable characters are ignored. + /// </summary> + public bool HasConsistentLeadingWhitespace + { + get + { + int distinctCount = _tab == 0 ? 0 : 1; + distinctCount += _space == 0 ? 0 : 1; + + return distinctCount <= 1; + } + } + + /// <summary> + /// Returns the kind of leading character that appears most in the document between tabs and spaces. + /// If they are exactly tied, this will pick the default value. + /// </summary> + public LineLeadingCharacter GetLeadingWhitespaceCharacter(LineLeadingCharacter defaultValue) + { + if (_tab > _space) + { + return LineLeadingCharacter.Tab; + } + else if (_space > _tab) + { + return LineLeadingCharacter.Space; + } + else + { + return defaultValue; + } + } + } +} diff --git a/src/Editor/Text/Def/TextData/Document/NewlineState.cs b/src/Editor/Text/Def/TextData/Document/NewlineState.cs new file mode 100644 index 0000000..5353a32 --- /dev/null +++ b/src/Editor/Text/Def/TextData/Document/NewlineState.cs @@ -0,0 +1,138 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// NewlineState contains counts of each kind of supported newline, and can perform basic logic based on those counts. One of + /// these is created for each open document, and is kept up-to-date by watching edits through the lifetime of the document. + /// </summary> + internal class NewlineState + { + /// <summary> + /// Describes the supported types of newlines. + /// </summary> + public enum LineEnding + { + CRLF, + CR, + LF, + NEL, // unicode Next Line 0085 + LS, // unicode Line Separator 2028 + PS, // unicode Paragraph Separator 2029 + } + + // Counts for the various kinds of newlines. Internal for testing + internal int _cr; + internal int _lf; + internal int _crlf; + internal int _nel; + internal int _ls; + internal int _ps; + + /// <summary> + /// Increments the count for a specifica line ending by the provided number + /// </summary> + /// <param name="lineEnding">Line ending to be adjusted</param> + /// <param name="count">May be any positive or negative value</param> + public void Increment(LineEnding lineEnding, int count) + { + switch (lineEnding) + { + case LineEnding.CRLF: + _crlf += count; + break; + case LineEnding.CR: + _cr += count; + break; + case LineEnding.LF: + _lf += count; + break; + case LineEnding.NEL: + _nel += count; + break; + case LineEnding.LS: + _ls += count; + break; + case LineEnding.PS: + _ps += count; + break; + default: + throw new ArgumentException($"Unknown line ending value {lineEnding}", nameof(lineEnding)); + } + } + + /// <summary> + /// Gets whether the line endings are in a consistent state (either 0 or 1 kind of newline in the document). + /// </summary> + public bool HasConsistentLineEndings + { + get + { + int numDistinctLineEndings = _cr == 0 ? 0 : 1; + numDistinctLineEndings += _lf == 0 ? 0 : 1; + numDistinctLineEndings += _crlf == 0 ? 0 : 1; + numDistinctLineEndings += _nel == 0 ? 0 : 1; + numDistinctLineEndings += _ls == 0 ? 0 : 1; + numDistinctLineEndings += _ps == 0 ? 0 : 1; + + return numDistinctLineEndings <= 1; + } + } + + /// <summary> + /// If <see cref="HasConsistentLineEndings"/> is true, and there is at least one newline in the document, + /// this will return the kind of line ending found in the document. To determine what kind of newline + /// the document will use if there are no newlines, callers must inspect editor options. + /// </summary> + public LineEnding? InferredLineEnding + { + get + { + if (!HasConsistentLineEndings) + { + return null; + } + + // Checks below are roughly sorted in order of expected occurrances. + if (_crlf != 0) + { + return LineEnding.CRLF; + } + + if (_lf != 0) + { + return LineEnding.LF; + } + + if (_cr != 0) + { + return LineEnding.CR; + } + + if (_nel != 0) + { + return LineEnding.NEL; + } + + if (_ls != 0) + { + return LineEnding.LS; + } + + if (_ps != 0) + { + return LineEnding.PS; + } + + return null; + } + } + } +} diff --git a/src/Editor/Text/Def/TextLogic/Editor/ConvertTabsToSpacesContext.cs b/src/Editor/Text/Def/TextLogic/Editor/ConvertTabsToSpacesContext.cs new file mode 100644 index 0000000..baab084 --- /dev/null +++ b/src/Editor/Text/Def/TextLogic/Editor/ConvertTabsToSpacesContext.cs @@ -0,0 +1,15 @@ +namespace Microsoft.VisualStudio.Text.Editor +{ + internal class ConvertTabsToSpacesContext + { + public static readonly ConvertTabsToSpacesContext FromCodingConventions = new ConvertTabsToSpacesContext(true); + public static readonly ConvertTabsToSpacesContext FromToolsOptions = new ConvertTabsToSpacesContext(false); + + public bool SettingFromCodingConventions { get; } + + private ConvertTabsToSpacesContext(bool settingFromCodingConventions) + { + this.SettingFromCodingConventions = settingFromCodingConventions; + } + } +} diff --git a/src/Editor/Text/Def/TextLogic/Editor/IIndentationManagerService.cs b/src/Editor/Text/Def/TextLogic/Editor/IIndentationManagerService.cs new file mode 100644 index 0000000..352ae5a --- /dev/null +++ b/src/Editor/Text/Def/TextLogic/Editor/IIndentationManagerService.cs @@ -0,0 +1,48 @@ +// +// 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.Editor +{ + /// <summary> + /// This is a service that supports smart indentation in a file. + /// </summary> + /// <remarks> + /// <remarks>This is a MEF component part, and implementations should use the following to import it: + /// <code> + /// [Import] + /// IIndentationManagerService IndentationManagerService = null; + /// </code> + /// </remarks> + public interface IIndentationManagerService + { + /// <summary> + /// Get the desired indentation behavior for the specified <paramref name="buffer"/>. + /// </summary> + /// <param name="explicitFormat">true if the format is due to an explicit user request (e.g. format selection); false if the format is a side-effect of some user action (e.g. typing a newline).</param> + /// <param name="convertTabsToSpaces">True if tabs should be converted to spaces.</param> + /// <param name="tabSize">Desired tab size.</param> + /// <param name="indentSize">Desired indentation.</param> + void GetIndentation(ITextBuffer buffer, bool explicitFormat, out bool convertTabsToSpaces, out int tabSize, out int indentSize); + + /// <summary> + /// Determines whether spaces or tab should be used for <paramref name="buffer"/> when formatting. + /// </summary> + /// <param name="buffer">A position on the line of text being formatted.</param> + /// <param name="explicitFormat">true if the format is due to an explicit user request (e.g. format selection); false if the format is a side-effect of some user action (e.g. typing a newline).</param> + /// <returns>true if spaces should be used.</returns> + bool UseSpacesForWhitespace(ITextBuffer buffer, bool explicitFormat); + + /// <summary> + /// Determines the appropriate tab size for <paramref name="buffer"/> when formatting. + /// </summary> + int GetTabSize(ITextBuffer buffer, bool explicitFormat); + + /// <summary> + /// Determines the appropriate indentation size for <paramref name="buffer"/> when formatting. + /// </summary> + int GetIndentSize(ITextBuffer buffer, bool explicitFormat); + + } +} diff --git a/src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs b/src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs index 5ddac79..5bd26be 100644 --- a/src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs +++ b/src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs @@ -289,12 +289,17 @@ namespace Microsoft.VisualStudio.Text.Editor internal const string RemoteControlledResponsiveCompletionOptionName = "RemoteControlledResponsiveCompletion"; /// <summary> - /// Option that keeps track of whether user toggled the <see cref="DiagnosticModeOptionId"/>. - /// If set to true, Editor will produce a detailed log for a particular scenario of interest. + /// This option is no longer used. Back when it was used, + /// if set to true, Editor produced a detailed log for a particular scenario of interest. /// </summary> internal static readonly EditorOptionKey<bool> DiagnosticModeOptionId = new EditorOptionKey<bool>(DiagnosticModeOptionName); internal const string DiagnosticModeOptionName = "DiagnosticMode"; + /// <summary> + /// Determines whether automatic formatting should adapt to the contents of the file instead of user options. + /// </summary> + public static readonly EditorOptionKey<bool> AdaptiveFormattingOptionId = new EditorOptionKey<bool>(AdaptiveFormattingOptionName); + public const string AdaptiveFormattingOptionName = "AdaptiveFormatting"; #endregion } @@ -655,8 +660,7 @@ namespace Microsoft.VisualStudio.Text.Editor } /// <summary> - /// The option definition that puts Editor in a special diagnostic mode - /// where <c>DiagnosticLogger</c> class stores logs that can be later retrieved from a crash dump. + /// This option is no longer used /// </summary> [Export(typeof(EditorOptionDefinition))] [Name(DefaultOptions.DiagnosticModeOptionName)] @@ -666,5 +670,16 @@ namespace Microsoft.VisualStudio.Text.Editor public override EditorOptionKey<bool> Key => DefaultOptions.DiagnosticModeOptionId; } + /// <summary> + /// Determines whether automatic formatting should adapt to the contents of the file instead of user options. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(DefaultOptions.AdaptiveFormattingOptionName)] + internal sealed class AdaptiveFormattingOption : EditorOptionDefinition<bool> + { + public override bool Default => true; + public override EditorOptionKey<bool> Key => DefaultOptions.AdaptiveFormattingOptionId; + } + #endregion } diff --git a/src/Editor/Text/Def/TextUI/Commanding/CommandingConstants.cs b/src/Editor/Text/Def/TextUI/Commanding/CommandingConstants.cs new file mode 100644 index 0000000..685c746 --- /dev/null +++ b/src/Editor/Text/Def/TextUI/Commanding/CommandingConstants.cs @@ -0,0 +1,7 @@ +namespace Microsoft.VisualStudio.Commanding +{ + internal class CommandingConstants + { + internal const string AdditionalCommandExecutionContext = "Additional Command Execution Context"; + } +} diff --git a/src/Editor/Text/Impl/Commanding/EditorCommandHandlerService.cs b/src/Editor/Text/Impl/Commanding/EditorCommandHandlerService.cs index 60f6262..d855c7f 100644 --- a/src/Editor/Text/Impl/Commanding/EditorCommandHandlerService.cs +++ b/src/Editor/Text/Impl/Commanding/EditorCommandHandlerService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; @@ -56,53 +56,42 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation { if (!_factory.JoinableTaskContext.IsOnMainThread) { - throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.GetCommandState)} method shoudl only be called on the UI thread."); + throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.GetCommandState)} method should only be called on the UI thread."); } - // In Razor scenario it's possible that EditorCommandHandlerService is called re-entrantly, - // first by contained language command filter and then by editor command chain. - // To preserve Razor commanding semantics, only execute handlers once. - if (IsReentrantCall()) + // Build up chain of handlers per buffer + Func<CommandState> handlerChain = nextCommandHandler ?? UnavalableCommandFunc; + foreach (var bufferAndHandler in GetOrderedBuffersAndCommandHandlers<T>().Reverse()) { - return nextCommandHandler?.Invoke() ?? CommandState.Unavailable; - } - - using (var reentrancyGuard = new ReentrancyGuard(_textView)) - { - // Build up chain of handlers per buffer - Func<CommandState> handlerChain = nextCommandHandler ?? UnavalableCommandFunc; - foreach (var bufferAndHandler in GetOrderedBuffersAndCommandHandlers<T>().Reverse()) + T args = null; + // Declare locals to ensure that we don't end up capturing the wrong thing + var nextHandler = handlerChain; + var handler = bufferAndHandler.handler; + args = args ?? (args = argsFactory(_textView, bufferAndHandler.buffer)); + if (args == null) { - T args = null; - // Declare locals to ensure that we don't end up capturing the wrong thing - var nextHandler = handlerChain; - var handler = bufferAndHandler.handler; - args = args ?? (args = argsFactory(_textView, bufferAndHandler.buffer)); - if (args == null) - { - // Args factory failed, skip command handlers and just call next - return handlerChain(); - } - - handlerChain = () => handler.GetCommandState(args, nextHandler); + // Args factory failed, skip command handlers and just call next + return handlerChain(); } - // Kick off the first command handler - return handlerChain(); + handlerChain = () => handler.GetCommandState(args, nextHandler); } + + // Kick off the first command handler + return handlerChain(); } public void Execute<T>(Func<ITextView, ITextBuffer, T> argsFactory, Action nextCommandHandler) where T : EditorCommandArgs { if (!_factory.JoinableTaskContext.IsOnMainThread) { - throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.Execute)} method shoudl only be called on the UI thread."); + throw new InvalidOperationException($"{nameof(IEditorCommandHandlerService.Execute)} method should only be called on the UI thread."); } - // In Razor scenario it's possible that EditorCommandHandlerService is called re-entrantly, - // first by contained language command filter and then by editor command chain. - // To preserve Razor commanding semantics, only execute handlers once. - if (IsReentrantCall()) + // In contained languge (Razor) scenario it's possible that EditorCommandHandlerService is called re-entrantly + // for the same command, first by contained language command filter and then by editor command chain. + // To preserve Razor commanding semantics, only execute handlers once for the same command. + if (IsReentrantCall<T>()) { nextCommandHandler?.Invoke(); return; @@ -110,7 +99,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation EditorCommandHandlerServiceState state = null; - using (var reentrancyGuard = new ReentrancyGuard(_textView)) + using (var reentrancyGuard = new ReentrancyGuard<T>(_textView)) { // Build up chain of handlers per buffer Action handlerChain = nextCommandHandler ?? EmptyAction; @@ -180,7 +169,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation // Per internal convention hosts can add additional host specific input context properties into // text view's property bag. We then surface it to command handlers (first item in case it's a list) via // CommandExecutionContext properties using type name as a key. - if (_textView.Properties.TryGetProperty("Additional Command Execution Context", out object hostSpecificInputContext)) + if (_textView.Properties.TryGetProperty(CommandingConstants.AdditionalCommandExecutionContext, out object hostSpecificInputContext)) { if (hostSpecificInputContext != null) { @@ -253,25 +242,42 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation nextCommandHandler?.Invoke(); } - private class ReentrancyGuard : IDisposable + // Guards against re-entrant execution of the same command (can happen in contained language scenario + // where two command handler services are chained together). + // The guard works by placing a key composed of the ReentrancyGuard's type and the type of command + // being executed into text view's property bag. + private class ReentrancyGuard<T> : IDisposable + where T : EditorCommandArgs { private readonly IPropertyOwner _owner; public ReentrancyGuard(IPropertyOwner owner) { _owner = owner ?? throw new ArgumentNullException(nameof(owner)); - _owner.Properties[typeof(ReentrancyGuard)] = this; + _owner.Properties[GetGuardKey()] = this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (Type, Type) GetGuardKey() + { + return (typeof(ReentrancyGuard<>), typeof(T)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsReentrantCall(IPropertyOwner owner) + { + return owner.Properties.ContainsProperty((typeof(ReentrancyGuard<>), typeof(T))); } public void Dispose() { - _owner.Properties.RemoveProperty(typeof(ReentrancyGuard)); + _owner.Properties.RemoveProperty(GetGuardKey()); } } - private bool IsReentrantCall() + private bool IsReentrantCall<T>() where T : EditorCommandArgs { - return _textView.Properties.ContainsProperty(typeof(ReentrancyGuard)); + return ReentrancyGuard<T>.IsReentrantCall(_textView); } //internal for unit tests @@ -356,7 +362,8 @@ 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 (_factory.ContentTypeComparer.Compare(handler.Metadata.ContentTypes, currentHandler.Metadata.ContentTypes) < 0) + if (_factory.ContentTypeOrderer.IsMoreSpecific(candidate: handler.Metadata.ContentTypes, + current: currentHandler.Metadata.ContentTypes)) { foundBetterHandler = true; handlerBuckets[i].Pop(); @@ -499,6 +506,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation private readonly EditorCommandHandlerServiceState _state; private readonly ITextView _textView; private readonly ILoggingServiceInternal _loggingService; + private INamed _timedOutCommandHandler; public TimeoutController(EditorCommandHandlerServiceState state, ITextView textView, ILoggingServiceInternal loggingService) { @@ -508,13 +516,14 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation } public int CancelAfter - => _state.IsExecutingTypingCommand ? + => _state.IsExecutingTypingCommand && _textView.Options.GetOptionValue(DefaultOptions.EnableTypingLatencyGuardOptionId) ? _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 + // Grab currently executing command handler as by the time it's cancelled and OnTimeout() is called it migth be gone. + _timedOutCommandHandler = _state.GetCurrentlyExecutingCommandHander(); return _state.IsExecutingTypingCommand; } @@ -526,7 +535,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation _loggingService?.PostEvent($"{TelemetryEventPrefix}/ExecutionTimeout", $"{TelemetryPropertyPrefix}.Command", executingCommand?.GetType().FullName, - $"{TelemetryPropertyPrefix}.CommandHandler", _state.GetCurrentlyExecutingCommandHander()?.GetType().FullName, + $"{TelemetryPropertyPrefix}.CommandHandler", _timedOutCommandHandler?.GetType().FullName, $"{TelemetryPropertyPrefix}.Timeout", this.CancelAfter, $"{TelemetryPropertyPrefix}.WasExecutionCancelled", wasExecutionCancelled); } diff --git a/src/Editor/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs b/src/Editor/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs index a2eac95..f2b5d6f 100644 --- a/src/Editor/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs +++ b/src/Editor/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs @@ -28,7 +28,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation IStatusBarService statusBar, IContentTypeRegistryService contentTypeRegistryService, IGuardedOperations guardedOperations, - [Import(AllowDefault = true)] ILoggingServiceInternal loggingService) + ILoggingServiceInternal loggingService) { UIThreadOperationExecutor = uiThreadOperationExecutor; JoinableTaskContext = joinableTaskContext; @@ -37,7 +37,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation LoggingService = loggingService; _contentTypeRegistryService = contentTypeRegistryService; - ContentTypeComparer = new StableContentTypeComparer(_contentTypeRegistryService); + ContentTypeOrderer = new StableContentTypeOrderer<ICommandHandler, ICommandHandlerMetadata>(_contentTypeRegistryService); _commandHandlers = OrderCommandHandlers(commandHandlers); if (!bufferResolvers.Any()) { @@ -57,7 +57,7 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation internal IStatusBarService StatusBar { get; } - internal StableContentTypeComparer ContentTypeComparer { get; } + internal StableContentTypeOrderer<ICommandHandler, ICommandHandlerMetadata> ContentTypeOrderer { get; } public IEditorCommandHandlerService GetService(ITextView textView) { @@ -85,9 +85,10 @@ namespace Microsoft.VisualStudio.UI.Text.Commanding.Implementation return new EditorCommandHandlerService(this, textView, _commandHandlers, new SingleBufferResolver(subjectBuffer)); } - private IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> OrderCommandHandlers(IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> commandHandlers) + // internal for unit tests + internal IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> OrderCommandHandlers(IEnumerable<Lazy<ICommandHandler, ICommandHandlerMetadata>> commandHandlers) { - return commandHandlers.OrderBy((handler) => handler.Metadata.ContentTypes, ContentTypeComparer); + return this.ContentTypeOrderer.Order(commandHandlers); } } } diff --git a/src/Editor/Text/Impl/TextModel/WhitespaceManager.cs b/src/Editor/Text/Impl/TextModel/WhitespaceManager.cs new file mode 100644 index 0000000..4b0eaa0 --- /dev/null +++ b/src/Editor/Text/Impl/TextModel/WhitespaceManager.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using Microsoft.VisualStudio.Text.Data.Utilities; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Document +{ + internal class WhitespaceManager : IWhitespaceManager + { + public WhitespaceManager(ITextBuffer documentBuffer, NewlineState newlineState, LeadingWhitespaceState leadingWhitespaceState) + { + documentBuffer.Changed += this.OnDocumentBufferChanged; + NewlineState = newlineState; + LeadingWhitespaceState = leadingWhitespaceState; + } + + public NewlineState NewlineState { get; private set; } + public LeadingWhitespaceState LeadingWhitespaceState { get; private set; } + + private void OnDocumentBufferChanged(object sender, TextContentChangedEventArgs e) + { + FrugalList<Span> oldLineBreakLines = null; // Note: these are all spans of line numbers, not character positions. + FrugalList<Span> newLineBreakLines = null; + + FrugalList<Span> oldWhitespaceLines = null; + FrugalList<Span> newWhitespaceLines = null; + + for (int i = 0; i < e.Changes.Count; i++) + { + var change = e.Changes[i]; + AddLineBreakLines(ref oldLineBreakLines, e.Before, change.OldSpan); + AddLineBreakLines(ref newLineBreakLines, e.After, change.NewSpan); + + AddWhitespaceLines(ref oldWhitespaceLines, e.Before, change.OldSpan); + AddWhitespaceLines(ref newWhitespaceLines, e.After, change.NewSpan); + } + + this.UpdateNewLines(e.Before, oldLineBreakLines, -1); + this.UpdateNewLines(e.After, newLineBreakLines, 1); + + this.UpdateWhitespace(e.Before, oldWhitespaceLines, -1); + this.UpdateWhitespace(e.After, newWhitespaceLines, 1); + } + + // Add the range of line numbers on snapshot whose line endings might be affected by a change to span. + private static void AddLineBreakLines(ref FrugalList<Span> lineBreakLines, ITextSnapshot snapshot, Span span) + { + var startLine = snapshot.GetLineFromPosition(span.Start); + var endLine = (span.End < startLine.EndIncludingLineBreak) ? startLine : snapshot.GetLineFromPosition(span.End); + + // Extend the range if the span starts at the start of a line (since it could affect the line break of the previous line) + // or touches the line break at the end of the line). + int startLineNumber = ((span.Start == startLine.Start) && (span.Start != 0)) ? (startLine.LineNumber - 1) : startLine.LineNumber; + int endLineNumber = (span.End < endLine.End) ? endLine.LineNumber : (endLine.LineNumber + 1); + + AddSpanToLines(ref lineBreakLines, startLineNumber, endLineNumber); + } + + // Add the range of line numbers on snapshot whose leading whitespace might be affected by a change to span. + private static void AddWhitespaceLines(ref FrugalList<Span> whitespaceLines, ITextSnapshot snapshot, Span span) + { + var startLine = snapshot.GetLineFromPosition(span.Start); + var endLine = (span.End < startLine.EndIncludingLineBreak) ? startLine : snapshot.GetLineFromPosition(span.End); + + // Changes that don't start at the beginning of a line can't affect the starting character of that line. + int startLineNumber = (span.Start == startLine.Start) ? startLine.LineNumber : (startLine.LineNumber + 1); + int endLineNumber = endLine.LineNumber + 1; + + AddSpanToLines(ref whitespaceLines, startLineNumber, endLineNumber); + } + + private static void AddSpanToLines(ref FrugalList<Span> lines, int startLineNumber, int endLineNumber) + { + if (startLineNumber != endLineNumber) + { + if (lines == null) + lines = new FrugalList<Span>(); + + lines.Add(Span.FromBounds(startLineNumber, endLineNumber)); + } + } + + private void UpdateNewLines(ITextSnapshot snapshot, FrugalList<Span> lineSpans, int delta) + { + if (lineSpans != null) + { + var collection = (lineSpans.Count == 1) ? ((IReadOnlyList<Span>)lineSpans) : new NormalizedSpanCollection(lineSpans); + for (int i = 0; (i < collection.Count); ++i) + { + Span lineSpan = collection[i]; + for (int line = lineSpan.Start; (line < lineSpan.End); ++line) + { + ITextSnapshotLine snapshotLine = snapshot.GetLineFromLineNumber(line); + var state = snapshotLine.GetLineEnding(); + if (state.HasValue) + this.NewlineState.Increment(state.Value, delta); + } + } + } + } + + private void UpdateWhitespace(ITextSnapshot snapshot, FrugalList<Span> lineSpans, int delta) + { + if (lineSpans != null) + { + var collection = (lineSpans.Count == 1) ? ((IReadOnlyList<Span>)lineSpans) : new NormalizedSpanCollection(lineSpans); + for (int i = 0; (i < collection.Count); ++i) + { + Span lineSpan = collection[i]; + for (int line = lineSpan.Start; (line < lineSpan.End); ++line) + { + ITextSnapshotLine snapshotLine = snapshot.GetLineFromLineNumber(line); + var state = snapshotLine.GetLeadingCharacter(); + this.LeadingWhitespaceState.Increment(state, delta); + } + } + } + } + } +} diff --git a/src/Editor/Text/Impl/TextModel/WhitespaceManagerFactory.cs b/src/Editor/Text/Impl/TextModel/WhitespaceManagerFactory.cs new file mode 100644 index 0000000..86cfc24 --- /dev/null +++ b/src/Editor/Text/Impl/TextModel/WhitespaceManagerFactory.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.ComponentModel.Composition.Primitives; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Text.Document; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + [Export(typeof(IWhitespaceManagerFactory))] + internal class WhitespaceManagerFactory : IWhitespaceManagerFactory + { + public IWhitespaceManager GetOrCreateWhitespaceManager( + ITextBuffer buffer, + NewlineState initialNewlineState, + LeadingWhitespaceState initialLeadingWhitespaceState) + { + return buffer.Properties.GetOrCreateSingletonProperty( + typeof(IWhitespaceManager), + () => new WhitespaceManager(buffer, initialNewlineState, initialLeadingWhitespaceState)); + } + + public bool TryGetExistingWhitespaceManager(ITextBuffer buffer, out IWhitespaceManager manager) + { + return buffer.Properties.TryGetProperty(typeof(IWhitespaceManager), out manager); + } + } +} diff --git a/src/Editor/Text/Util/TextDataUtil/StableContentTypeOrderer.cs b/src/Editor/Text/Util/TextDataUtil/StableContentTypeOrderer.cs new file mode 100644 index 0000000..013a6db --- /dev/null +++ b/src/Editor/Text/Util/TextDataUtil/StableContentTypeOrderer.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Custom orderer for sorting lists of items associated with content types that preserves original order of items with unrelated content types. + /// </summary> + internal class StableContentTypeOrderer<T, M> where M : IContentTypeMetadata + { + private readonly IContentTypeRegistryService _contentTypeRegistryService; + + public StableContentTypeOrderer(IContentTypeRegistryService contentTypeRegistryService) + { + _contentTypeRegistryService = contentTypeRegistryService ?? throw new ArgumentNullException(nameof(contentTypeRegistryService)); + } + + internal IEnumerable<Lazy<T, M>> Order(IEnumerable<Lazy<T, M>> items) + { + return StableTopologicalSort.Order(items, ContentTypeOrderDependencyFunction); + } + + /// <summary> + /// An element dependency function used to by topological orderer to detect whether items depend on each other based on + /// their content type metadata. For example an item with [ContentType("CSharp")] depends on an item with + /// [ContentType("text")] because "CSharp" inherits "text". + /// </summary> + /// <returns> + /// <c>true</c> if any content type in intemY inherits any content type in itemX, <c>false</c> otherwise. + /// </returns> + private bool ContentTypeOrderDependencyFunction(Lazy<T, M> itemX, Lazy<T, M> itemY) + { + var current = itemX.Metadata.ContentTypes; + var candidate = itemY.Metadata.ContentTypes; + + return IsMoreSpecific(candidate, current); + } + + internal bool IsMoreSpecific(IEnumerable<string> candidate, IEnumerable<string> current) + { + foreach (var candidateContentTypeStr in candidate) + { + var candidateContentType = _contentTypeRegistryService.GetContentType(candidateContentTypeStr); + if (candidateContentType != null) + { + foreach (var currentContentTypeStr in current) + { + // IContentType.IsOfType returns true for the same content type, while we need to know only + // if one inherits another. + if (!string.Equals(candidateContentTypeStr, currentContentTypeStr, StringComparison.OrdinalIgnoreCase) && + candidateContentType.IsOfType(currentContentTypeStr)) + { + return true; + } + } + } + } + + return false; + } + } +} diff --git a/src/Editor/Text/Util/TextDataUtil/WhitespaceExtensions.cs b/src/Editor/Text/Util/TextDataUtil/WhitespaceExtensions.cs new file mode 100644 index 0000000..b8714da --- /dev/null +++ b/src/Editor/Text/Util/TextDataUtil/WhitespaceExtensions.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Text.Data.Utilities +{ + internal static class WhitespaceExtensions + { + public const string CRLF_LITERAL = "\r\n"; + public const string CR_LITERAL = "\r"; + public const string LF_LITERAL = "\n"; + public const string NEL_LITERAL = "\u0085"; + public const string LS_LITERAL = "\u2028"; + public const string PS_LITERAL = "\u2029"; + + /// <summary> + /// Given a line, returns the kind of newline that appears at the end of the line. + /// </summary> + /// <param name="line">The line to inspect</param> + /// <returns>The kind of newline appearing at the end of the line, or null if this is the last line of the document.</returns> + public static NewlineState.LineEnding? GetLineEnding(this ITextSnapshotLine line) + { + if (line.LineBreakLength == 0) + { + return null; + } + + if (line.LineBreakLength == 2) + { + return NewlineState.LineEnding.CRLF; + } + + switch (line.Snapshot[line.End]) + { + case '\r': + return NewlineState.LineEnding.CR; + case '\n': + return NewlineState.LineEnding.LF; + case '\u0085': + return NewlineState.LineEnding.NEL; + case '\u2028': + return NewlineState.LineEnding.LS; + case '\u2029': + return NewlineState.LineEnding.PS; + default: + throw new ArgumentException($"Unexpected newline character {line.Snapshot[line.End]}", nameof(line)); + } + } + + public static LeadingWhitespaceState.LineLeadingCharacter GetLeadingCharacter(this ITextSnapshotLine line) + { + if (line.Length == 0) + { + return LeadingWhitespaceState.LineLeadingCharacter.Empty; + } + + switch (line.Snapshot[line.Start]) + { + case ' ': + return LeadingWhitespaceState.LineLeadingCharacter.Space; + case '\t': + return LeadingWhitespaceState.LineLeadingCharacter.Tab; + default: + return LeadingWhitespaceState.LineLeadingCharacter.Printable; + } + } + + /// <summary> + /// Takes a string representation of a line ending and returns the corresponding line ending. + /// </summary> + /// <remarks>This method will involve allocating strings, if at all posibile, use LineEndingFromSnapshotLine.</remarks> + /// <param name="lineEndingString">A string representation of a line ending.</param> + /// <returns>The corresponding LineEnding enumeration value. Null if the string isn't a recognized line ending.</returns> + public static NewlineState.LineEnding? LineEndingFromString(string lineEndingString) + { + switch (lineEndingString) + { + case CRLF_LITERAL: + return NewlineState.LineEnding.CRLF; + case CR_LITERAL: + return NewlineState.LineEnding.CR; + case LF_LITERAL: + return NewlineState.LineEnding.LF; + case NEL_LITERAL: + return NewlineState.LineEnding.NEL; + case LS_LITERAL: + return NewlineState.LineEnding.LS; + case PS_LITERAL: + return NewlineState.LineEnding.PS; + default: + return null; + } + } + + public static string StringFromLineEnding(this NewlineState.LineEnding lineEnding) + { + switch (lineEnding) + { + case NewlineState.LineEnding.CRLF: + return CRLF_LITERAL; + case NewlineState.LineEnding.CR: + return CR_LITERAL; + case NewlineState.LineEnding.LF: + return LF_LITERAL; + case NewlineState.LineEnding.NEL: + return NEL_LITERAL; + case NewlineState.LineEnding.LS: + return LS_LITERAL; + case NewlineState.LineEnding.PS: + return PS_LITERAL; + default: + // We shouldn't have any more, just return CRLF as paranoia. + return CRLF_LITERAL; + } + } + + /// <summary> + /// Normalizes the given buffer to match the given newline string on every line + /// </summary> + /// <returns>True if the buffer was changed. False otherwise.</returns> + public static bool NormalizeNewlines(this ITextBuffer buffer, string newlineString) + { + using (var edit = buffer.CreateEdit()) + { + foreach (var line in edit.Snapshot.Lines) + { + if (line.LineBreakLength != 0) + { + // Calling line.GetLineBreakText allocates a string. Since we only have 1 2-character newline to worry about + // we can do this without that allocation by comparing characters directly. + if (line.LineBreakLength != newlineString.Length || edit.Snapshot[line.End] != newlineString[0]) + { + // Intentionally ignore failed replaces. We'll do the best effort change here. + edit.Replace(new Span(line.End, line.LineBreakLength), newlineString); + } + } + } + + if (edit.HasEffectiveChanges) + { + return edit.Apply() != edit.Snapshot; + } + else + { + // We didn't have to do anything + return false; + } + } + } + + /// <summary> + /// Normalizes the given buffer to match the given whitespace stype. + /// </summary> + /// <returns>True if the buffer was changed. False otherwise.</returns> + public static bool NormalizeLeadingWhitespace(this ITextBuffer buffer, int tabSize, bool useSpaces) + { + using (var edit = buffer.CreateEdit()) + { + var whitespaceCache = new string[100]; + + foreach (var line in edit.Snapshot.Lines) + { + if (line.Length > 0) + { + AnalyzeWhitespace(line, tabSize, out int whitespaceCharacterLength, out int column); + if (column > 0) + { + var whitespace = GetWhitespace(whitespaceCache, tabSize, useSpaces, column); + + if ((whitespace.Length != whitespaceCharacterLength) || !ComparePrefix(line, whitespace)) + { + edit.Replace(new Span(line.Start, whitespaceCharacterLength), whitespace); + } + } + } + } + + return edit.Apply() != edit.Snapshot; + } + } + + private static void AnalyzeWhitespace(ITextSnapshotLine line, int tabSize, out int whitespaceCharacterLength, out int column) + { + column = 0; + whitespaceCharacterLength = 0; + while (whitespaceCharacterLength < line.Length) + { + var c = (line.Start + whitespaceCharacterLength).GetChar(); + if (c == ' ') + ++column; + else if (c == '\t') + column = ((1 + column / tabSize) * tabSize); + else + break; + + ++whitespaceCharacterLength; + } + } + + private static string GetWhitespace(string[] whitespaceCache, int tabSize, bool useSpaces, int column) + { + string whitespace; + if ((column < whitespaceCache.Length) && (whitespaceCache[column] != null)) + { + whitespace = whitespaceCache[column]; + } + else + { + if (useSpaces) + { + whitespace = new string(' ', column); + } + else + { + whitespace = new string('\t', column / tabSize); + var spaces = column % tabSize; + if (spaces != 0) + whitespace += new string(' ', spaces); + } + + if (column < whitespaceCache.Length) + whitespaceCache[column] = whitespace; + } + + return whitespace; + } + + private static bool ComparePrefix(ITextSnapshotLine line, string whitespace) + { + for (int i = 0; (i < whitespace.Length); ++i) + if ((line.Start + i).GetChar() != whitespace[i]) + return false; + + return true; + } + + /// <summary> + /// Normalizes the given buffer to match the given newline state's line endings if they are consistent. + /// </summary> + /// <returns>True if the buffer was changed. False otherwise.</returns> + public static bool NormalizeNewlines(this NewlineState newlineState, ITextBuffer buffer) + { + // Keep this method in sync with the other NormalizeNewlines overload below. + if (!newlineState.HasConsistentLineEndings || !newlineState.InferredLineEnding.HasValue) + { + // Right now we expect people to overwhelmingly start from project templates, item templates, or cloned code, + // which will give them at least one newline in a given document. If they don't then we're taking the easy + // route here, to not do anything. That could be improved upon, but we're waiting for user feedback to justify + // further work in this area. + return false; + } + + /* Potential optimization, check to see if text contains any newlines that would be replaced, and if not, just return text and avoid allocations */ + string newlineString = newlineState.InferredLineEnding.Value.StringFromLineEnding(); + + return buffer.NormalizeNewlines(newlineString); + } + + public static string NormalizeNewlines(this NewlineState newlineState, string text) + { + // Keep this method in sync with the other NormalizeNewlines overload above + if (!newlineState.HasConsistentLineEndings || !newlineState.InferredLineEnding.HasValue) + { + // Right now we expect people to overwhelmingly start from project templates, item templates, or cloned code, + // which will give them at least one newline in a given document. If they don't then we're taking the easy + // route here, to not do anything. That could be improved upon, but we're waiting for user feedback to justify + // further work in this area. + return text; + } + + /* Potential optimization, check to see if text contains any newlines that would be replaced, and if not, just return text and avoid allocations */ + string newlineString = newlineState.InferredLineEnding.Value.StringFromLineEnding(); + + // In the perverse case, where we have a string full of "\n\n\n\n\n\n" and the document wants \r\n, we can only ever double the size of the string. + var strBuilder = new StringBuilder(text.Length * 2); + + for (int i = 0; i < text.Length; i++) + { + switch (text[i]) + { + case '\r': + if (i < (text.Length - 1) && text[i + 1] == '\n') + { + i++; + } + goto case '\n'; + case '\n': + case '\u0085': + case '\u2028': + case '\u2029': + strBuilder.Append(newlineString); + break; + default: + strBuilder.Append(text[i]); + break; + } + } + + return strBuilder.ToString(); + + } + } +} |