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

github.com/microsoft/vs-editor-api.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSandy Armstrong <sandy@xamarin.com>2019-10-28 18:14:29 +0300
committerSandy Armstrong <sandy@xamarin.com>2019-10-28 18:14:29 +0300
commit0306a5f676a0fbf184328aa932abe37a4923af17 (patch)
tree3f3e1612d463f0b9034a36aba568433be2cbef70
parentdc60fc684aaa490dcc4e4d768704bbfe81aa858c (diff)
Sync with vs-editor-core@140e98831
-rw-r--r--src/Editor/Text/Def/TextData/AssemblyInfo.cs20
-rw-r--r--src/Editor/Text/Def/TextData/Document/IWhitespaceManager.cs22
-rw-r--r--src/Editor/Text/Def/TextData/Document/IWhitespaceManagerFactory.cs43
-rw-r--r--src/Editor/Text/Def/TextData/Document/LeadingWhitespaceState.cs101
-rw-r--r--src/Editor/Text/Def/TextData/Document/NewlineState.cs138
-rw-r--r--src/Editor/Text/Def/TextLogic/Editor/ConvertTabsToSpacesContext.cs15
-rw-r--r--src/Editor/Text/Def/TextLogic/Editor/IIndentationManagerService.cs48
-rw-r--r--src/Editor/Text/Def/TextLogic/EditorOptions/DefaultOptions.cs23
-rw-r--r--src/Editor/Text/Def/TextUI/Commanding/CommandingConstants.cs7
-rw-r--r--src/Editor/Text/Impl/Commanding/EditorCommandHandlerService.cs97
-rw-r--r--src/Editor/Text/Impl/Commanding/EditorCommandHandlerServiceFactory.cs11
-rw-r--r--src/Editor/Text/Impl/TextModel/WhitespaceManager.cs119
-rw-r--r--src/Editor/Text/Impl/TextModel/WhitespaceManagerFactory.cs30
-rw-r--r--src/Editor/Text/Util/TextDataUtil/StableContentTypeOrderer.cs62
-rw-r--r--src/Editor/Text/Util/TextDataUtil/WhitespaceExtensions.cs305
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();
+
+ }
+ }
+}