diff options
author | Kirill Osenkov <KirillOsenkov@users.noreply.github.com> | 2017-10-10 23:56:02 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-10 23:56:02 +0300 |
commit | 745eea2fa0c121b43f4d0f4148e290e8310e86ef (patch) | |
tree | 5c95f83e5f4126112bb58d64961aa658f8d8d99c | |
parent | 9859bbb12de1f629927f4109e9105616b7d8f3f8 (diff) |
Initial commit of the non-WPF implementation layer. (#7)
289 files changed, 54480 insertions, 448 deletions
diff --git a/Directory.build.props b/Directory.build.props new file mode 100644 index 0000000..9ad5ef8 --- /dev/null +++ b/Directory.build.props @@ -0,0 +1,14 @@ +<Project> + <PropertyGroup> + <RootDirectory>$(MSBuildThisFileDirectory)</RootDirectory> + <OutDir>$(RootDirectory)\bin\$(Configuration)\$(MSBuildProjectName)\</OutDir> + <BaseIntermediateOutputPath>$(RootDirectory)\obj\$(Configuration)\$(MSBuildProjectName)\</BaseIntermediateOutputPath> + <IntermediateOutputPath>$(RootDirectory)\obj\$(Configuration)\$(MSBuildProjectName)\</IntermediateOutputPath> + </PropertyGroup> + + <PropertyGroup> + <MicrosoftVisualStudioThreadingVersion>15.3.23</MicrosoftVisualStudioThreadingVersion> + <MicrosoftVisualStudioUtilitiesVersion>15.0.26606</MicrosoftVisualStudioUtilitiesVersion> + <MicrosoftVisualStudioValidationVersion>15.3.15</MicrosoftVisualStudioValidationVersion> + </PropertyGroup> +</Project>
\ No newline at end of file diff --git a/Editor.sln b/Editor.sln new file mode 100644 index 0000000..0a4f81e --- /dev/null +++ b/Editor.sln @@ -0,0 +1,48 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26730.3 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.VisualStudio.Text.Implementation", "src\Microsoft.VisualStudio.Text.Implementation.csproj", "{BE7BC037-1934-4C01-8CC6-3D041F07AF2B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoreUtility", "src\Core\Def\CoreUtility.csproj", "{0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextData", "src\Text\Def\TextData\TextData.csproj", "{DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextLogic", "src\Text\Def\TextLogic\TextLogic.csproj", "{593E6B4A-038E-4126-9051-4C72DCA1D52E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextUI", "src\Text\Def\TextUI\TextUI.csproj", "{E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BE7BC037-1934-4C01-8CC6-3D041F07AF2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE7BC037-1934-4C01-8CC6-3D041F07AF2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE7BC037-1934-4C01-8CC6-3D041F07AF2B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE7BC037-1934-4C01-8CC6-3D041F07AF2B}.Release|Any CPU.Build.0 = Release|Any CPU + {0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E1CDA83-67F4-4E7C-A5C8-EA735CE8BDB2}.Release|Any CPU.Build.0 = Release|Any CPU + {DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DA075D67-7AEB-4D98-B2B3-296AE3ED0CAC}.Release|Any CPU.Build.0 = Release|Any CPU + {593E6B4A-038E-4126-9051-4C72DCA1D52E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {593E6B4A-038E-4126-9051-4C72DCA1D52E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {593E6B4A-038E-4126-9051-4C72DCA1D52E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {593E6B4A-038E-4126-9051-4C72DCA1D52E}.Release|Any CPU.Build.0 = Release|Any CPU + {E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8410FDE-EAE0-4113-9A3D-67AE985C9FC9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {02CA45CA-E68B-4F34-891E-09831EEE4E54} + EndGlobalSection +EndGlobal diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..9023385 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <packageSources> + <add key="Nuget Official" value ="https://api.nuget.org/v3/index.json" /> + <add key="VS Editor" value="https://www.myget.org/F/vs-editor/" /> + </packageSources> +</configuration> diff --git a/src/Core/Def/BaseUtility/IGuardedOperations.cs b/src/Core/Def/BaseUtility/IGuardedOperations.cs new file mode 100644 index 0000000..839018f --- /dev/null +++ b/src/Core/Def/BaseUtility/IGuardedOperations.cs @@ -0,0 +1,181 @@ +// +// 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.Threading.Tasks; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Operations that guard calls to extensions code and log errors. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + public interface IGuardedOperations + { + /// <summary> + /// Makes a guarded call to an extension point. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + void CallExtensionPoint(Action call); + + /// <summary> + /// Makes a guarded call to an extension point. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + void CallExtensionPoint(object errorSource, Action call); + + /// <summary> + /// Makes a guarded call to an extension point. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + T CallExtensionPoint<T>(Func<T> call, T valueOnThrow); + + /// <summary> + /// Makes a guarded call to an extension point. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + T CallExtensionPoint<T>(object errorSource, Func<T> call, T valueOnThrow); + + /// <summary> + /// Makes a guarded call to an async extension point. + /// </summary> + /// <param name="asyncAction">The extension point to be called.</param> + /// <returns>A <see cref="Task"/> that asynchronously executes the <paramref name="asyncAction"/>.</returns> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + Task CallExtensionPointAsync(Func<Task> asyncAction); + + /// <summary> + /// Makes a guarded call to an async extension point. + /// </summary> + /// <param name="asyncAction">The extension point to be called.</param> + /// <returns>A <see cref="Task"/> that asynchronously executes the <paramref name="asyncAction"/>.</returns> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + Task CallExtensionPointAsync(object errorSource, Func<Task> asyncAction); + + /// <summary> + /// Makes a guarded call to an async extension point. + /// </summary> + /// <typeparam name="T">The type of the value returned from the <paramref name="asyncCall"/>.</typeparam> + /// <param name="asyncCall">The extension point to be called.</param> + /// <param name="valueOnThrow">The value returned if call failed.</param> + /// <returns>A <see cref="Task{T}"/> that asynchronously executes the <paramref name="asyncCall"/>.</returns> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + Task<T> CallExtensionPointAsync<T>(Func<Task<T>> asyncCall, T valueOnThrow); + + /// <summary> + /// Makes a guarded call to an async extension point. + /// </summary> + /// <typeparam name="T">The type of the value returned from the <paramref name="asyncCall"/>.</typeparam> + /// <param name="asyncCall">The extension point to be called.</param> + /// <param name="valueOnThrow">The value returned if call failed.</param> + /// <returns>A <see cref="Task{T}"/> that asynchronously executes the <paramref name="asyncCall"/>.</returns> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + Task<T> CallExtensionPointAsync<T>(object errorSource, Func<Task<T>> asyncCall, T valueOnThrow); + + /// <summary> + /// Selects eligible extension factories. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + IEnumerable<Lazy<TExtensionFactory, TMetadataView>> FindEligibleFactories<TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService) + where TExtensionFactory : class + where TMetadataView : INamedContentTypeMetadata; + + /// <summary> + /// Handles an exception occured in a call to an extension point. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + void HandleException(object errorSource, Exception e); + + /// <summary> + /// Safely instantiates an extension point. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + TExtension InstantiateExtension<TExtension>(object errorSource, Lazy<TExtension> provider); + + /// <summary> + /// Safely instantiates an extension point. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + TExtension InstantiateExtension<TExtension, TMetadata>(object errorSource, Lazy<TExtension, TMetadata> provider); + + /// <summary> + /// Safely instantiates an extension point. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + TExtensionInstance InstantiateExtension<TExtension, TMetadata, TExtensionInstance>(object errorSource, Lazy<TExtension, TMetadata> provider, Func<TExtension, TExtensionInstance> getter); + + /// <summary> + /// Safely invokes best matching extension factory. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + TExtension InvokeBestMatchingFactory<TExtension, TMetadataView>(IList<Lazy<TExtension, TMetadataView>> providerHandles, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService, object errorSource) where TMetadataView : IContentTypeMetadata; + + /// <summary> + /// Safely invokes best matching extension factory. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + TExtensionInstance InvokeBestMatchingFactory<TExtensionFactory, TExtensionInstance, TMetadataView>(IList<Lazy<TExtensionFactory, TMetadataView>> providerHandles, IContentType dataContentType, Func<TExtensionFactory, TExtensionInstance> getter, IContentTypeRegistryService contentTypeRegistryService, object errorSource) + where TExtensionFactory : class + where TMetadataView : IContentTypeMetadata; + + /// <summary> + /// Safely invokes all eligible extension factories. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + List<TExtensionInstance> InvokeEligibleFactories<TExtensionInstance, TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, Func<TExtensionFactory, TExtensionInstance> getter, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService, object errorSource) + where TExtensionInstance : class + where TExtensionFactory : class + where TMetadataView : INamedContentTypeMetadata; + + /// <summary> + /// Safely invokes all matching extension factories. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + List<TExtensionInstance> InvokeMatchingFactories<TExtensionInstance, TExtensionFactory, TMetadataView>(IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, Func<TExtensionFactory, TExtensionInstance> getter, IContentType dataContentType, object errorSource) + where TExtensionInstance : class + where TExtensionFactory : class + where TMetadataView : IContentTypeMetadata; + + /// <summary> + /// Safely raises an event. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + void RaiseEvent(object sender, EventHandler eventHandlers); + + /// <summary> + /// Safely raises an event. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + void RaiseEvent<TArgs>(object sender, EventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs; + + /// <summary> + /// Safely raises an event on a background thread. + /// </summary> + /// <remarks>This class supports the Visual Studio + /// infrastructure and in general is not intended to be used directly from your code.</remarks> + Task RaiseEventOnBackgroundAsync<TArgs>(object sender, AsyncEventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs; + } +} diff --git a/src/Core/Def/ContentType/IContentTypeMetadata.cs b/src/Core/Def/ContentType/IContentTypeMetadata.cs new file mode 100644 index 0000000..aeb5860 --- /dev/null +++ b/src/Core/Def/ContentType/IContentTypeMetadata.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Represents MEF metadata view corresponding to the <see cref="ContentTypeAttribute"/>s. + /// </summary> + public interface IContentTypeMetadata + { + /// <summary> + /// List of declared content types. + /// </summary> + IEnumerable<string> ContentTypes { get; } + } +} diff --git a/src/Core/Def/ContentType/IContentTypeRegistryService2.cs b/src/Core/Def/ContentType/IContentTypeRegistryService2.cs new file mode 100644 index 0000000..adaf2b1 --- /dev/null +++ b/src/Core/Def/ContentType/IContentTypeRegistryService2.cs @@ -0,0 +1,24 @@ +// +// 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.Utilities +{ + using System; + using Microsoft.VisualStudio.Utilities; + + public interface IContentTypeRegistryService2 : IContentTypeRegistryService + { + /// <summary> + /// Get the mime type associated with a content type. + /// </summary> + /// <remarks>Use the <see cref="MimeTypeAttribute"/> attribute on a <see cref="ContentTypeDefinition"/> to associate a mime type with a content type.</remarks> + string GetMimeType(IContentType type); + + /// <summary> + /// Get the content type associated with a mime type. + /// </summary> + /// <remarks>Use the <see cref="MimeTypeAttribute"/> attribute on a <see cref="ContentTypeDefinition"/> to associate a mime type with a content type.</remarks> + IContentType GetContentTypeForMimeType(string mimeType); + } +} diff --git a/src/Core/Def/ContentType/INameAndReplacesMetadata.cs b/src/Core/Def/ContentType/INameAndReplacesMetadata.cs new file mode 100644 index 0000000..16a7fd0 --- /dev/null +++ b/src/Core/Def/ContentType/INameAndReplacesMetadata.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System.Collections.Generic; +using System.ComponentModel; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Represents MEF metadata view corresponding to the <see cref="NameAttribute"/> and ReplacesAttributes. + /// </summary> + public interface INameAndReplacesMetadata + { + /// <summary> + /// Declared name value. + /// </summary> + [DefaultValue(null)] + string Name { get; } + + /// <summary> + /// Declared Replaces values. + /// </summary> + [DefaultValue(null)] + IEnumerable<string> Replaces { get; } + } +} diff --git a/src/Core/Def/ContentType/INamedContentTypeMetadata.cs b/src/Core/Def/ContentType/INamedContentTypeMetadata.cs new file mode 100644 index 0000000..4ae644b --- /dev/null +++ b/src/Core/Def/ContentType/INamedContentTypeMetadata.cs @@ -0,0 +1,13 @@ +// +// 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.Utilities +{ + /// <summary> + /// Represents MEF metadata view combining <see cref="IContentTypeMetadata"/> and <see cref="INameAndReplacesMetadata"/> views. + /// </summary> + public interface INamedContentTypeMetadata : IContentTypeMetadata, INameAndReplacesMetadata + { + } +} diff --git a/src/Core/Def/ContentType/MimeTypeAttribute.cs b/src/Core/Def/ContentType/MimeTypeAttribute.cs new file mode 100644 index 0000000..9935433 --- /dev/null +++ b/src/Core/Def/ContentType/MimeTypeAttribute.cs @@ -0,0 +1,41 @@ +// +// 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.Utilities +{ + using System; + using System.ComponentModel.Composition; + + /// <summary> + /// Declares an association between an extension part and a particular content type. + /// </summary> + [MetadataAttribute] + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method | AttributeTargets.Field, AllowMultiple = false)] + public sealed class MimeTypeAttribute : SingletonBaseMetadataAttribute + { + + /// <summary> + /// Initializes a new instance of <see cref="MimeTypeAttribute"/>. + /// </summary> + /// <param name="name">The Mime type to be associated with the content type.</param> + /// <exception cref="ArgumentNullException"><paramref name="name"/>is null or an empty string.</exception> + public MimeTypeAttribute(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + this.MimeType = name; + } + + /// <summary> + /// The MimeType for the content type definition + /// </summary> + public string MimeType + { + get; + } + } +} diff --git a/src/Core/Def/CoreUtility.csproj b/src/Core/Def/CoreUtility.csproj index f920b72..6426d95 100644 --- a/src/Core/Def/CoreUtility.csproj +++ b/src/Core/Def/CoreUtility.csproj @@ -1,75 +1,17 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <PropertyGroup Label="BuildProps"> - <BuildPropsFile>$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), Build.props))\Build.props</BuildPropsFile> - </PropertyGroup> - <Import Project="$(BuildPropsFile)" Condition="'$(BuildProps_Imported)'!='True' AND Exists('$(BuildPropsFile)') AND '$(VisualStudioDir)'==''" /> - <Import Project="..\Platform.Settings.targets" /> - <Import Project="$(PlatformPath)\Tools\Targets\Platform.Settings.Selector.targets" /> +<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0"> <PropertyGroup> <AssemblyName>Microsoft.VisualStudio.CoreUtility</AssemblyName> - <OutputType>Library</OutputType> - <OutputPath>$(BinariesDirectory)\bin\$(BuildArchitecture)</OutputPath> - <SignAssemblyAttribute>true</SignAssemblyAttribute> - <UseVsVersion>true</UseVsVersion> - <AssemblyAttributeClsCompliant>true</AssemblyAttributeClsCompliant> - <GenerateAssemblyRefs>true</GenerateAssemblyRefs> - <NoWarn>649;436;$(NoWarn)</NoWarn> - <GeneratedModuleId>Microsoft.VisualStudio.CoreUtility</GeneratedModuleId> - <GeneratedModuleVersion>$(VsAssemblyVersion)</GeneratedModuleVersion> - <BuildArchitecturesAllowed>$(BuildArchitecturesAllowed);amd64;arm</BuildArchitecturesAllowed> - </PropertyGroup> - <!-- IDE specific Information --> - <PropertyGroup> - <ProjectGuid>{BA3DD7EC-3F13-4400-A3A9-96AD425B3369}</ProjectGuid> + <TargetFramework>net46</TargetFramework> </PropertyGroup> + <ItemGroup> - <Compile Include="BaseUtility\ITelemetryIdProvider.cs" /> - <Compile Include="CoreUtilityAssemblyInfo.cs" /> - <Compile Include="BaseUtility\SingletonBaseMetadataAttribute.cs" /> - <Compile Include="BaseUtility\MultipleBaseMetadataAttribute.cs" /> - <Compile Include="BaseUtility\BaseDefinitionAttribute.cs" /> - <Compile Include="BaseUtility\DisplayNameAttribute.cs" /> - <Compile Include="BaseUtility\FxCopSuppressions.cs" /> - <Compile Include="BaseUtility\IOrderable.cs" /> - <Compile Include="BaseUtility\IPropertyOwner.cs" /> - <Compile Include="BaseUtility\NameAttribute.cs" /> - <Compile Include="BaseUtility\OrderAttribute.cs" /> - <Compile Include="BaseUtility\Orderer.cs" /> - <Compile Include="BaseUtility\PropertyCollection.cs" /> - <Compile Include="ContentType\FileExtensionToContentTypeDefinition.cs" /> - <Compile Include="ContentType\FileExtensionAttribute.cs" /> - <Compile Include="ContentType\FileNameAttribute.cs" /> - <Compile Include="ContentType\IContentTypeDefinition.cs" /> - <Compile Include="ContentType\IContentTypeDefinitionSource.cs" /> - <Compile Include="ContentType\IContentTypeRegistryService.cs" /> - <Compile Include="ContentType\IFileExtensionRegistryService.cs" /> - <Compile Include="ContentType\IFileExtensionRegistryService2.cs" /> - <Compile Include="ContentType\ContentTypeAttribute.cs" /> - <Compile Include="ContentType\ContentTypeDefinition.cs" /> - <Compile Include="ContentType\FxCopSuppressions.cs" /> - <Compile Include="ContentType\IContentType.cs" /> - <Reference Include="System" /> - <Reference Include="System.Core" /> - <Reference Include="System.ComponentModel.Composition" /> - <CopyFile Include="BaseUtility\Microsoft.VisualStudio.Utilities.Overview.mht"> - <DestFolder>$(SuiteBinPath)\PlatformOverviews</DestFolder> - <Visible>false</Visible> - </CopyFile> - <CopyFile Include="BaseUtility\Microsoft.VisualStudio.Utilities.UsageGuide.mht"> - <DestFolder>$(SuiteBinPath)\PlatformOverviews</DestFolder> - <Visible>false</Visible> - </CopyFile> + <PackageReference Include="Microsoft.VisualStudio.Threading" Version="$(MicrosoftVisualStudioThreadingVersion)" /> + <PackageReference Include="Microsoft.VisualStudio.Validation" Version="$(MicrosoftVisualStudioValidationVersion)" /> </ItemGroup> + <ItemGroup> - <PublishPartCompiled Include="$(OutputPath)\$(AssemblyName).dll"> - <Visibility>Inter</Visibility> - <FileType>Reference</FileType> - </PublishPartCompiled> + <Reference Include="System.ComponentModel.Composition" /> </ItemGroup> - <!--Import the targets--> - <Import Project="$(PlatformPath)\Tools\Targets\Platform.Imports.targets" /> - <PropertyGroup> - <CopyToSuiteBin>true</CopyToSuiteBin> - </PropertyGroup> + </Project>
\ No newline at end of file diff --git a/src/Core/Impl/ContentType/ContentTypeImpl.cs b/src/Core/Impl/ContentType/ContentTypeImpl.cs new file mode 100644 index 0000000..5a05136 --- /dev/null +++ b/src/Core/Impl/ContentType/ContentTypeImpl.cs @@ -0,0 +1,188 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Utilities.Implementation +{ + internal partial class ContentTypeImpl : IContentType + { + private readonly string name; + private readonly static IReadOnlyList<ContentTypeImpl> emptyBaseTypes = new ContentTypeImpl[0]; + private IReadOnlyList<ContentTypeImpl> baseTypeList = emptyBaseTypes; + + internal ContentTypeImpl(string name, string mimeType = null, IEnumerable<string> baseTypes = null) + { + this.name = name; + this.MimeType = mimeType; + this.UnprocessedBaseTypes = baseTypes; + } + + public string TypeName + { + get { return this.name; } + } + + public string DisplayName + { + get { return this.name; } + } + + public string MimeType { get; } + + public bool IsOfType(string type) + { + if (String.Compare(type, this.name, StringComparison.OrdinalIgnoreCase) == 0) + { + return true; + } + else + { + foreach (IContentType baseType in this.baseTypeList) + { + if (baseType.IsOfType(type)) + { + return true; + } + } + } + return false; + } + + public IEnumerable<IContentType> BaseTypes + { + get { return this.baseTypeList; } + } + + public override string ToString() + { + return this.name; + } + + internal void ProcessBaseTypes(IDictionary<string, ContentTypeImpl> nameToContentTypeBuilder, + IDictionary<string, ContentTypeImpl> mimeTypeToContentTypeBuilder) + { + if (this.UnprocessedBaseTypes != null) + { + List<ContentTypeImpl> newBaseTypes = new List<ContentTypeImpl>(); + foreach (var baseTypeName in this.UnprocessedBaseTypes) + { + // The expectation is that the base type will already exists but (if it doesn't) add a stub for it (& the ctor for a stub base type leaves it in a state + // where the basetypes/state are set appropriately). + var baseType = ContentTypeRegistryImpl.AddContentTypeFromMetadata(baseTypeName, /* mime type */null, /* base types */null, nameToContentTypeBuilder, mimeTypeToContentTypeBuilder); + if (baseType == ContentTypeRegistryImpl.UnknownContentTypeImpl) + { + throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, + Strings.ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown, this.TypeName)); + } + + if (!newBaseTypes.Contains(baseType)) + newBaseTypes.Add(baseType); + } + + if (newBaseTypes.Count > 0) + { + this.baseTypeList = newBaseTypes.ToArray(); + this.state = VisitState.NotVisited; + } + else + { + Debug.Assert(object.ReferenceEquals(this.baseTypeList, emptyBaseTypes)); + } + + this.UnprocessedBaseTypes = null; + } + } + + // used internally for cycle detection + internal enum VisitState + { + NotVisited = 0, // The node hasn't been visited yet + Visiting, // The node (or one of its children) is being visited + Visited // The node and its children have been visited before + } + + private VisitState state = VisitState.Visited; + + internal bool CheckForCycle(bool breakCycle) + { + try + { + if (this.baseTypeList.Count != 0) + { + this.state = VisitState.Visiting; + foreach (var baseType in this.baseTypeList) + { + if (baseType.state == VisitState.Visiting) + { + if (breakCycle) + { + // There is a cycle of this -> basetype -> ... -> this + // Don't try a surgical fix: simply break the cycle the easiest way possible + // since this is an error in the definitions that shouldn't happen. + // TODO: log the error. + this.baseTypeList = emptyBaseTypes; + } + + return true; + } + else if ((baseType.state == VisitState.NotVisited) && baseType.CheckForCycle(breakCycle)) + { + return true; + } + } + } + } + finally + { + this.state = VisitState.Visited; + } + + return false; + } + + // used internally when building up content types + internal void AddUnprocessedBaseTypes(IEnumerable<string> newBaseTypes) + { + if (newBaseTypes != null) + { + if (object.ReferenceEquals(this.UnprocessedBaseTypes, emptyBaseTypes)) + { + this.UnprocessedBaseTypes = newBaseTypes; + } + else + { + var allBaseTypes = new List<string>(this.UnprocessedBaseTypes); + allBaseTypes.AddRange(newBaseTypes); + this.UnprocessedBaseTypes = allBaseTypes; + } + } + } + + internal IEnumerable<string> UnprocessedBaseTypes; + +#if DEBUG + internal bool IsProcessed + { + get + { + return (this.UnprocessedBaseTypes == null) && (this.baseTypeList != null) && + ((this.baseTypeList.Count == 0) + ? (object.ReferenceEquals(this.baseTypeList, ContentTypeImpl.emptyBaseTypes) && (this.state == VisitState.Visited)) + : (this.state == VisitState.NotVisited)); + } + } + + internal bool IsCheckedForCycles + { + get { return (this.state == VisitState.Visited) && (this.UnprocessedBaseTypes == null) && (object.ReferenceEquals(this.baseTypeList, ContentTypeImpl.emptyBaseTypes) || (this.baseTypeList.Count > 0)); } + } +#endif + } +} diff --git a/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs b/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs new file mode 100644 index 0000000..a5586a2 --- /dev/null +++ b/src/Core/Impl/ContentType/ContentTypeRegistryServiceImpl.cs @@ -0,0 +1,789 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel.Composition; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Microsoft.VisualStudio.Utilities.Implementation +{ + public interface IContentTypeDefinitionMetadata + { + string Name { get; } + + [System.ComponentModel.DefaultValue(null)] + IEnumerable<string> BaseDefinition { get; } + + [System.ComponentModel.DefaultValue(null)] + string MimeType { get; } + } + + [Export(typeof(IFileExtensionRegistryService))] + [Export(typeof(IFileExtensionRegistryService2))] + [Export(typeof(IContentTypeRegistryService))] + [Export(typeof(IContentTypeRegistryService2))] + internal sealed partial class ContentTypeRegistryImpl : IContentTypeRegistryService2, IFileExtensionRegistryService, IFileExtensionRegistryService2 + { + [ImportMany] + internal List<Lazy<ContentTypeDefinition, IContentTypeDefinitionMetadata>> ContentTypeDefinitions { get; set; } + + [ImportMany] + internal List<IContentTypeDefinitionSource> ExternalSources { get; set; } + + [ImportMany] + internal List<Lazy<FileExtensionToContentTypeDefinition, IFileToContentTypeMetadata>> FileToContentTypeProductions { get; set; } + + private MapCollection maps; + + /// <summary> + /// The name of the unknown content type, guaranteed to exists no matter what other content types are produced + /// </summary> + private const string UnknownContentTypeName = "UNKNOWN"; + internal readonly static ContentTypeImpl UnknownContentTypeImpl = new ContentTypeImpl(ContentTypeRegistryImpl.UnknownContentTypeName, null, null); + + /// <summary> + /// Builds the list of available content types + /// Note: This function must be called after acquiring a lock on syncLock + /// </summary> + /// <remarks> + /// Building the content type mappings should not throw exceptions, but should rather be logging issues + /// with some kind of common error reporting service and try to recover by ignoring the asset productions + /// that are deemed to cause the problem. + /// </remarks> + private void BuildContentTypes() + { + var oldMaps = Volatile.Read(ref this.maps); + if (oldMaps == null) + { + var nameToContentTypeBuilder = MapCollection.Empty.NameToContentTypeMap.ToBuilder(); + var mimeTypeToContentTypeBuilder = MapCollection.Empty.MimeTypeToContentTypeMap.ToBuilder(); + + // Add the singleton Unknown content type to the dictionary + nameToContentTypeBuilder.Add(ContentTypeRegistryImpl.UnknownContentTypeName, ContentTypeRegistryImpl.UnknownContentTypeImpl); + + + // For each content type provision, create an IContentType. + foreach (Lazy<ContentTypeDefinition, IContentTypeDefinitionMetadata> contentTypeDefinition in ContentTypeDefinitions) + { + AddContentTypeFromMetadata(contentTypeDefinition.Metadata.Name, + contentTypeDefinition.Metadata.MimeType, + contentTypeDefinition.Metadata.BaseDefinition, nameToContentTypeBuilder, mimeTypeToContentTypeBuilder); + } + + // Now consider the external sources. This allows us to consider legacy content types together with MEF-defined + // content types. + foreach (IContentTypeDefinitionSource source in this.ExternalSources) + { + if (source.Definitions != null) + { + foreach (IContentTypeDefinition metadata in source.Definitions) + { + AddContentTypeFromMetadata(metadata.Name, + /* mimeType*/ null, + metadata.BaseDefinitions, nameToContentTypeBuilder, mimeTypeToContentTypeBuilder); + } + } + } + + List<ContentTypeImpl> allTypes = new List<ContentTypeImpl>(nameToContentTypeBuilder.Count); + allTypes.AddRange(nameToContentTypeBuilder.Values); + foreach (var type in allTypes) + { + type.ProcessBaseTypes(nameToContentTypeBuilder, mimeTypeToContentTypeBuilder); + } + +#if DEBUG + foreach (var type in nameToContentTypeBuilder.Values) + { + Debug.Assert(type.IsProcessed); + } +#endif + + foreach (var type in nameToContentTypeBuilder.Values) + { + type.CheckForCycle(breakCycle: true); + } + + var fileExtensionToContentTypeMapBuilder = MapCollection.Empty.FileExtensionToContentTypeMap.ToBuilder(); + var fileNameToContentTypeMapBuilder = MapCollection.Empty.FileNameToContentTypeMap.ToBuilder(); + foreach (var fileExtensionDefinition in this.FileToContentTypeProductions) + { + // MEF ensures that there will be at least one content type in the metadata. We take the first one. + // We prefer this over defining a different attribute from ContentType[] for this purpose. + var contentTypeName = fileExtensionDefinition.Metadata.ContentTypes.FirstOrDefault(); + ContentTypeImpl contentType; + if ((contentTypeName != null) && nameToContentTypeBuilder.TryGetValue(contentTypeName, out contentType)) + { + if (!string.IsNullOrEmpty(fileExtensionDefinition.Metadata.FileExtension)) + { + foreach (var ext in fileExtensionDefinition.Metadata.FileExtension.Split(';')) + { + if (ext != null) + { + var extension = RemoveExtensionDot(ext); + if (!(string.IsNullOrWhiteSpace(extension) || fileExtensionToContentTypeMapBuilder.ContainsKey(extension))) + fileExtensionToContentTypeMapBuilder.Add(extension, contentType); + } + } + } + + if (!string.IsNullOrEmpty(fileExtensionDefinition.Metadata.FileName)) + { + foreach (var name in fileExtensionDefinition.Metadata.FileName.Split(';')) + { + if (!(string.IsNullOrWhiteSpace(name) || fileNameToContentTypeMapBuilder.ContainsKey(name))) + fileNameToContentTypeMapBuilder.Add(name, contentType); + } + } + } + } + + var newMaps = new MapCollection(nameToContentTypeBuilder.ToImmutable(), mimeTypeToContentTypeBuilder.ToImmutable(), fileExtensionToContentTypeMapBuilder.ToImmutable(), fileNameToContentTypeMapBuilder.ToImmutable()); + Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps); + + // We actually don't care whether or not the CompareExchange succeeded. + // Eitehr it succeeded (normally the case) or someone else successfully completed BuildContentTypes on another thread and we shouldn't do anything. + } + } + + private const string BaseMimePrefix = @"text/"; + private const string MimePrefix = BaseMimePrefix + @"x-"; + + internal static ContentTypeImpl AddContentTypeFromMetadata(string contentTypeName, string mimeType, IEnumerable<string> baseTypes, + IDictionary<string, ContentTypeImpl> nameToContentTypeBuilder, + IDictionary<string, ContentTypeImpl> mimeTypeToContentTypeBuilder) + { + if (!string.IsNullOrEmpty(contentTypeName)) + { + ContentTypeImpl type; + if (!nameToContentTypeBuilder.TryGetValue(contentTypeName, out type)) + { + bool addToMimeTypeMap = false; + if (string.IsNullOrWhiteSpace(mimeType)) + { + mimeType = MimePrefix + contentTypeName.ToLowerInvariant(); + } + else if (mimeTypeToContentTypeBuilder.ContainsKey(mimeType)) + { + mimeType = null; + } + else + { + addToMimeTypeMap = true; + } + + type = new ContentTypeImpl(contentTypeName, mimeType, baseTypes); + + nameToContentTypeBuilder.Add(contentTypeName, type); + if (addToMimeTypeMap) + { + mimeTypeToContentTypeBuilder.Add(mimeType, type); + } + } + else + { + type.AddUnprocessedBaseTypes(baseTypes); + } + + return type; + } + + return null; + } + + /// <summary> + /// Checks whether the specified type is base type for another content type + /// </summary> + /// <param name="typeToCheck">The type to check for being a base type</param> + /// <param name="derivedType">An out parameter to receive the first discovered derived type</param> + /// <returns><c>True</c> if the given <paramref name="typeToCheck"/> content type is a base type</returns> + private bool IsBaseType(ContentTypeImpl typeToCheck, out ContentTypeImpl derivedType) + { + derivedType = null; + + foreach (ContentTypeImpl type in this.maps.NameToContentTypeMap.Values) + { + if (type != typeToCheck) + { + foreach (IContentType baseType in type.BaseTypes) + { + if (baseType == typeToCheck) + { + derivedType = type; + return true; + } + } + } + } + + return false; + } + + #region IContentTypeRegistryService Members + public IContentType GetContentType(string typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + { + throw new ArgumentException(nameof(typeName)); + } + + this.BuildContentTypes(); + + ContentTypeImpl contentType = null; + this.maps.NameToContentTypeMap.TryGetValue(typeName, out contentType); + + return contentType; + } + + public IContentType UnknownContentType + { + get { return ContentTypeRegistryImpl.UnknownContentTypeImpl; } + } + + public IEnumerable<IContentType> ContentTypes + { + get + { + this.BuildContentTypes(); + var map = this.maps.NameToContentTypeMap; + + return map.Values; + } + } + + public IContentType AddContentType(string typeName, IEnumerable<string> baseTypeNames) + { + if (string.IsNullOrWhiteSpace(typeName)) + { + throw new ArgumentException(nameof(typeName)); + } + + // This has the side effect of building the content types. + if (this.GetContentType(typeName) != null) + { + // Cannot dynamically add a new content type if a content type with the same name already exists + throw new ArgumentException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotAddExistentType, typeName)); + } + + var oldMaps = Volatile.Read(ref this.maps); + while (true) + { + var nameToContentTypeMap = new PseudoBuilder<string, ContentTypeImpl>(oldMaps.NameToContentTypeMap); + var mimeTypeToContentTypeMap = new PseudoBuilder<string, ContentTypeImpl>(oldMaps.MimeTypeToContentTypeMap); + + var type = AddContentTypeFromMetadata(typeName, null, baseTypeNames, + nameToContentTypeMap, mimeTypeToContentTypeMap); + + type.ProcessBaseTypes(nameToContentTypeMap, mimeTypeToContentTypeMap); + + if (type.CheckForCycle(breakCycle: false)) + { + throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CausesCycles, type.TypeName)); + } + + var newMaps = new MapCollection(nameToContentTypeMap.Source, mimeTypeToContentTypeMap.Source, oldMaps.FileExtensionToContentTypeMap, oldMaps.FileNameToContentTypeMap); + var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps); + if (results == oldMaps) + { + return type; + } + + // Two people tried to add content types simultaneously. + oldMaps = results; + } + } + + public void RemoveContentType(string typeName) + { + if (string.IsNullOrWhiteSpace(typeName)) + { + throw new ArgumentException(nameof(typeName)); + } + + this.BuildContentTypes(); + + var oldMaps = Volatile.Read(ref this.maps); + while (true) + { + ContentTypeImpl type; + if (!oldMaps.NameToContentTypeMap.TryGetValue(typeName, out type)) + { + // No type == no type to remove; + return; + } + + if (type == ContentTypeRegistryImpl.UnknownContentTypeImpl) + { + // Check if the type to be removed is not the Unknown content type + throw new InvalidOperationException(Strings.ContentTypeRegistry_CannotRemoveTheUnknownType); + } + + ContentTypeImpl derivedType; + if (IsBaseType(type, out derivedType)) + { + // Check if the type is base type for another registered type + throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveBaseType, type.TypeName, derivedType.TypeName)); + } + + // If there are file extensions using this content type we won't allow removing it + if (this.maps.FileExtensionToContentTypeMap.Values.Any(c => c == type)) + { + // If there are file extensions using this content type we won't allow removing it + throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions, type.TypeName)); + } + + // If there are file extensions using this content type we won't allow removing it + if (this.maps.FileNameToContentTypeMap.Values.Any(c => c == type)) + { + // If there are file extensions using this content type we won't allow removing it + throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentUICulture, Strings.ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions, type.TypeName)); + } + + var newMaps = new MapCollection(oldMaps.NameToContentTypeMap.Remove(typeName), + (type.MimeType != null) ? oldMaps.MimeTypeToContentTypeMap.Remove(type.MimeType) : oldMaps.MimeTypeToContentTypeMap, + oldMaps.FileExtensionToContentTypeMap, oldMaps.FileNameToContentTypeMap); + var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps); + if (results == oldMaps) + { + return; + } + + // Two people tried to remove content types simultaneously. + oldMaps = results; + } + } + #endregion + + #region IContentTypeRegistryService2 Members + public string GetMimeType(IContentType type) + { + var typeImpl = type as ContentTypeImpl; + if (typeImpl == null) + { + throw new ArgumentException(nameof(type)); + } + else if (typeImpl == UnknownContentTypeImpl) + { + return null; + } + + return typeImpl.MimeType; + } + + public IContentType GetContentTypeForMimeType(string mimeType) + { + if (string.IsNullOrWhiteSpace(mimeType)) + { + throw new ArgumentException(nameof(mimeType)); + } + + this.BuildContentTypes(); + + ContentTypeImpl contentType = null; + if (!this.maps.MimeTypeToContentTypeMap.TryGetValue(mimeType, out contentType)) + { + if (mimeType.StartsWith(BaseMimePrefix)) + { + if (!(mimeType.StartsWith(MimePrefix) && this.maps.NameToContentTypeMap.TryGetValue(mimeType.Substring(MimePrefix.Length), out contentType))) + { + this.maps.NameToContentTypeMap.TryGetValue(mimeType.Substring(BaseMimePrefix.Length), out contentType); + } + } + } + + return contentType; + } + #endregion + + #region IFileExtensionRegistryService Members + public IContentType GetContentTypeForExtension(string extension) + { + if (extension == null) + { + throw new ArgumentNullException(nameof(extension)); + } + + this.BuildContentTypes(); + + ContentTypeImpl contentType = null; + this.maps.FileExtensionToContentTypeMap.TryGetValue(RemoveExtensionDot(extension), out contentType); + + // TODO: should we return null if contentType is null? + return contentType ?? ContentTypeRegistryImpl.UnknownContentTypeImpl; + } + + public IEnumerable<string> GetExtensionsForContentType(IContentType contentType) + { + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + + this.BuildContentTypes(); + + // We don't expect this to be called on a perf critical thread so we can use the dictionary. + foreach (var kvp in this.maps.FileExtensionToContentTypeMap) + { + if (contentType == kvp.Value) + { + yield return kvp.Key; + } + } + } + + public void AddFileExtension(string extension, IContentType contentType) + { + if (string.IsNullOrWhiteSpace(extension)) + { + throw new ArgumentException(nameof(extension)); + } + + var contentTypeImpl = contentType as ContentTypeImpl; + if ((contentTypeImpl == null) || (contentTypeImpl == UnknownContentTypeImpl)) + { + throw new ArgumentException(nameof(contentType)); + } + + this.BuildContentTypes(); + extension = RemoveExtensionDot(extension); + + var oldMaps = Volatile.Read(ref this.maps); + while (true) + { + ContentTypeImpl type; + if (oldMaps.FileExtensionToContentTypeMap.TryGetValue(extension, out type)) + { + if (type != contentTypeImpl) + { + throw new InvalidOperationException + (String.Format(System.Globalization.CultureInfo.CurrentUICulture, + Strings.FileExtensionRegistry_NoMultipleContentTypes, extension)); + } + + return; + } + + var newMaps = new MapCollection(oldMaps.NameToContentTypeMap, oldMaps.MimeTypeToContentTypeMap, + oldMaps.FileExtensionToContentTypeMap.Add(extension, contentTypeImpl), + oldMaps.FileNameToContentTypeMap); + + var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps); + if (results == oldMaps) + { + return; + } + + // Two people tried to remove content types simultaneously. + oldMaps = results; + } + } + + public void RemoveFileExtension(string extension) + { + if (extension == null) + { + throw new ArgumentNullException(nameof(extension)); + } + + this.BuildContentTypes(); + + extension = RemoveExtensionDot(extension); + + var oldMaps = Volatile.Read(ref this.maps); + while (true) + { + if (!oldMaps.FileExtensionToContentTypeMap.ContainsKey(extension)) + { + return; + } + + var newMaps = new MapCollection(oldMaps.NameToContentTypeMap, oldMaps.MimeTypeToContentTypeMap, + oldMaps.FileExtensionToContentTypeMap.Remove(extension), + oldMaps.FileNameToContentTypeMap); + + var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps); + if (results == oldMaps) + { + return; + } + + // Two people tried to remove content types simultaneously. + oldMaps = results; + } + } + #endregion + + #region IFileExtensionRegistryService2 Members + public IContentType GetContentTypeForFileName(string fileName) + { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + this.BuildContentTypes(); + + ContentTypeImpl contentType = null; + this.maps.FileNameToContentTypeMap.TryGetValue(fileName, out contentType); + + // TODO: should we return null if contentType is null? + return contentType ?? ContentTypeRegistryImpl.UnknownContentTypeImpl; + } + + public IContentType GetContentTypeForFileNameOrExtension(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + // No need to lock, we are calling locking public method. + var fileNameContentType = this.GetContentTypeForFileName(name); + + // Attempt to use extension as fallback ContentType if file name isn't recognized. + if (fileNameContentType == ContentTypeRegistryImpl.UnknownContentTypeImpl) + { + var extension = Path.GetExtension(name); + + if (!string.IsNullOrEmpty(extension)) + { + // No need to lock, we are calling locking public method. + return this.GetContentTypeForExtension(extension); + } + } + + return fileNameContentType; + } + + public IEnumerable<string> GetFileNamesForContentType(IContentType contentType) + { + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + + this.BuildContentTypes(); + + // We don't expect this to be called on a perf critical thread so we can use the dictionary. + foreach (var kvp in this.maps.FileNameToContentTypeMap) + { + if (contentType == kvp.Value) + { + yield return kvp.Key; + } + } + } + + public void AddFileName(string fileName, IContentType contentType) + { + if (string.IsNullOrWhiteSpace(fileName)) + { + throw new ArgumentException(nameof(fileName)); + } + + var contentTypeImpl = contentType as ContentTypeImpl; + if ((contentTypeImpl == null) || (contentTypeImpl == UnknownContentTypeImpl)) + { + throw new ArgumentException(nameof(contentType)); + } + + this.BuildContentTypes(); + + var oldMaps = Volatile.Read(ref this.maps); + while (true) + { + ContentTypeImpl type; + if (oldMaps.FileNameToContentTypeMap.TryGetValue(fileName, out type)) + { + if (type != contentTypeImpl) + { + throw new InvalidOperationException + (String.Format(System.Globalization.CultureInfo.CurrentUICulture, + Strings.FileExtensionRegistry_NoMultipleContentTypes, fileName)); + } + + return; + } + + var newMaps = new MapCollection(oldMaps.NameToContentTypeMap, oldMaps.MimeTypeToContentTypeMap, + oldMaps.FileExtensionToContentTypeMap, + oldMaps.FileNameToContentTypeMap.Add(fileName, contentTypeImpl)); + + var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps); + if (results == oldMaps) + { + return; + } + + // Two people tried to remove content types simultaneously. + oldMaps = results; + } + } + + public void RemoveFileName(string fileName) + { + if (fileName == null) + { + throw new ArgumentNullException(nameof(fileName)); + } + + this.BuildContentTypes(); + + var oldMaps = Volatile.Read(ref this.maps); + while (true) + { + if (!oldMaps.FileNameToContentTypeMap.ContainsKey(fileName)) + { + return; + } + + var newMaps = new MapCollection(oldMaps.NameToContentTypeMap, oldMaps.MimeTypeToContentTypeMap, + oldMaps.FileExtensionToContentTypeMap, + oldMaps.FileNameToContentTypeMap.Remove(fileName)); + + var results = Interlocked.CompareExchange(ref this.maps, newMaps, oldMaps); + if (results == oldMaps) + { + return; + } + + // Two people tried to remove content types simultaneously. + oldMaps = results; + } + } + #endregion + + private static string RemoveExtensionDot(string extension) + { + if (extension.StartsWith(".")) + { + return extension.TrimStart('.'); + } + else + { + return extension; + } + } + + class MapCollection + { + public readonly static MapCollection Empty = new MapCollection(); + + public readonly ImmutableDictionary<string, ContentTypeImpl> NameToContentTypeMap; + public readonly ImmutableDictionary<string, ContentTypeImpl> MimeTypeToContentTypeMap; + public readonly ImmutableDictionary<string, ContentTypeImpl> FileExtensionToContentTypeMap; + public readonly ImmutableDictionary<string, ContentTypeImpl> FileNameToContentTypeMap; + + private MapCollection() + { + this.NameToContentTypeMap = ImmutableDictionary<string, ContentTypeImpl>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase); + this.MimeTypeToContentTypeMap = ImmutableDictionary<string, ContentTypeImpl>.Empty.WithComparers(StringComparer.Ordinal); + this.FileExtensionToContentTypeMap = ImmutableDictionary<string, ContentTypeImpl>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase); + this.FileNameToContentTypeMap = ImmutableDictionary<string, ContentTypeImpl>.Empty.WithComparers(StringComparer.OrdinalIgnoreCase); + } + + public MapCollection(ImmutableDictionary<string, ContentTypeImpl> nameToContentType, ImmutableDictionary<string, ContentTypeImpl> mimeTypeToContentTypeMap, + ImmutableDictionary<string, ContentTypeImpl> fileExtensionToContentTypeMap, ImmutableDictionary<string, ContentTypeImpl> fileNameToContentTypeMap) + { + this.NameToContentTypeMap = nameToContentType; + this.MimeTypeToContentTypeMap = mimeTypeToContentTypeMap; + this.FileExtensionToContentTypeMap = fileExtensionToContentTypeMap; + this.FileNameToContentTypeMap = fileNameToContentTypeMap; + +#if DEBUG + foreach (var c in nameToContentType.Values) + { + Debug.Assert(c.IsCheckedForCycles); + } +#endif + } + } + + class PseudoBuilder<K, V> : IDictionary<K, V> + { + public ImmutableDictionary<K, V> Source { get; private set; } + + public PseudoBuilder(ImmutableDictionary<K, V> source) + { + this.Source = source; + } + + public void Add(K key, V value) + { + this.Source = this.Source.Add(key, value); + } + + public bool ContainsKey(K key) + { + return this.Source.ContainsKey(key); + } + + public bool TryGetValue(K key, out V value) + { + return this.Source.TryGetValue(key, out value); + } + + #region NotImplemented + public V this[K key] { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public ICollection<K> Keys => throw new NotImplementedException(); + + public ICollection<V> Values => throw new NotImplementedException(); + + public int Count => throw new NotImplementedException(); + + public bool IsReadOnly => throw new NotImplementedException(); + + public void Add(KeyValuePair<K, V> item) + { + throw new NotImplementedException(); + } + + public void Clear() + { + throw new NotImplementedException(); + } + + public bool Contains(KeyValuePair<K, V> item) + { + throw new NotImplementedException(); + } + + public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + public IEnumerator<KeyValuePair<K, V>> GetEnumerator() + { + throw new NotImplementedException(); + } + + public bool Remove(K key) + { + throw new NotImplementedException(); + } + + public bool Remove(KeyValuePair<K, V> item) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + #endregion + } + } +} diff --git a/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs new file mode 100644 index 0000000..4e3bbd9 --- /dev/null +++ b/src/Core/Impl/ContentType/IFileExtensionToContentTypeMetadata.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Utilities.Implementation +{ + using System; + using System.Collections.Generic; + + public interface IFileExtensionToContentTypeMetadata + { + string FileExtension { get; } + IEnumerable<string> ContentTypes { get; } + } +} + diff --git a/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs new file mode 100644 index 0000000..48ec87a --- /dev/null +++ b/src/Core/Impl/ContentType/IFileNameToContentTypeMetadata.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Utilities.Implementation +{ + using System; + using System.Collections.Generic; + + public interface IFileNameToContentTypeMetadata + { + string FileName { get; } + IEnumerable<string> ContentTypes { get; } + } +} + diff --git a/src/Core/Impl/ContentType/IFileToContentTypeMetadata.cs b/src/Core/Impl/ContentType/IFileToContentTypeMetadata.cs new file mode 100644 index 0000000..78dc2a6 --- /dev/null +++ b/src/Core/Impl/ContentType/IFileToContentTypeMetadata.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Utilities.Implementation +{ + using System; + using System.Collections.Generic; + + public interface IFileToContentTypeMetadata + { + [System.ComponentModel.DefaultValue(null)] + string FileExtension { get; } + + [System.ComponentModel.DefaultValue(null)] + string FileName { get; } + + IEnumerable<string> ContentTypes { get; } + } +} + diff --git a/src/Core/Impl/ContentType/StringToContentTypesMap.cs b/src/Core/Impl/ContentType/StringToContentTypesMap.cs new file mode 100644 index 0000000..52fae34 --- /dev/null +++ b/src/Core/Impl/ContentType/StringToContentTypesMap.cs @@ -0,0 +1,129 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Utilities.Implementation +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + + internal sealed class StringToContentTypesMap + { + // Map of extensions to their corresponding content types + private Dictionary<string, IContentType> stringMap; + + public StringToContentTypesMap(IEnumerable<Tuple<string, IContentType>> mappings) + { + + if (mappings == null) + { + throw new ArgumentNullException(nameof(mappings)); + } + + this.stringMap = new Dictionary<string, IContentType>(StringComparer.OrdinalIgnoreCase); + + foreach (var mapping in mappings) + { + // Any failures should ideally be logged somehow. + TryAddString(mapping.Item1, mapping.Item2); + } + } + + public IContentType GetContentTypeForString(string str) + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + + IContentType contentType; + + if (!this.stringMap.TryGetValue(str, out contentType)) + { + return null; + } + + return contentType; + } + + public IEnumerable<string> GetStringsForContentType(IContentType contentType) + { + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + + // Asymptotically slow, however, after searching the VS code base, we found that + // looking up extensions for ContentType is only used by tests, and is probably + // rarely used. This method used be backed by a second map for quick lookup, + // but for simplicity, we're going to move to a single map, barring any regressions. + return this.stringMap + .Where(pair => pair.Value == contentType) + .Select(pair => pair.Key); + } + + public void AddMapping(string str, IContentType contentType) + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + + if (!TryAddString(str, contentType)) + { + throw new InvalidOperationException + (String.Format(System.Globalization.CultureInfo.CurrentUICulture, + Strings.FileExtensionRegistry_NoMultipleContentTypes, str)); + } + } + + public void RemoveMapping(string str) + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + + this.stringMap.Remove(str); + } + + private bool TryAddString(string str, IContentType contentType) + { + if (str == null) + { + throw new ArgumentNullException(nameof(str)); + } + + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + + // Check if the string is already registered + IContentType existingContentType; + if (this.stringMap.TryGetValue(str, out existingContentType)) + { + // Return false if there is a conflict. + return contentType == existingContentType; + } + else + { + // A new string - lets add it to the map + this.stringMap.Add(str, contentType); + } + + return true; + } + } +} + diff --git a/src/Core/Impl/ContentType/Strings.Designer.cs b/src/Core/Impl/ContentType/Strings.Designer.cs new file mode 100644 index 0000000..736d04f --- /dev/null +++ b/src/Core/Impl/ContentType/Strings.Designer.cs @@ -0,0 +1,153 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.VisualStudio.Utilities.Implementation { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.CoreUtilityImplementation.ContentType.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to The content type {0} has already been defined.. + /// </summary> + internal static string ContentTypeRegistry_CannotAddExistentType { + get { + return ResourceManager.GetString("ContentTypeRegistry_CannotAddExistentType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot add content type with empty or undefined type name.. + /// </summary> + internal static string ContentTypeRegistry_CannotAddTypeWithEmptyTypeName { + get { + return ResourceManager.GetString("ContentTypeRegistry_CannotAddTypeWithEmptyTypeName", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to There can only be one "Unknown" content type defined. . + /// </summary> + internal static string ContentTypeRegistry_CannotProduceAnotherUnknownType { + get { + return ResourceManager.GetString("ContentTypeRegistry_CannotProduceAnotherUnknownType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type {0} cannot be removed because it's used as base type for the content type {1}.. + /// </summary> + internal static string ContentTypeRegistry_CannotRemoveBaseType { + get { + return ResourceManager.GetString("ContentTypeRegistry_CannotRemoveBaseType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The "Unknown" content type cannot be removed.. + /// </summary> + internal static string ContentTypeRegistry_CannotRemoveTheUnknownType { + get { + return ResourceManager.GetString("ContentTypeRegistry_CannotRemoveTheUnknownType", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The type {0} cannot be removed because it's associated with one or more file extensions.. + /// </summary> + internal static string ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions { + get { + return ResourceManager.GetString("ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The content type {0} cannot be used as base for content type {1} because it would create a derivation cycle.. + /// </summary> + internal static string ContentTypeRegistry_CausesCycles { + get { + return ResourceManager.GetString("ContentTypeRegistry_CausesCycles", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Content type {0} cannot be added. No content type can be derive from the "Unknown" content type. . + /// </summary> + internal static string ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown { + get { + return ResourceManager.GetString("ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unknown. + /// </summary> + internal static string ContentTypeRegistry_UnknownTypeDisplayName { + get { + return ResourceManager.GetString("ContentTypeRegistry_UnknownTypeDisplayName", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to {0} file name or extension cannot be associated with multiple content types.. + /// </summary> + internal static string FileExtensionRegistry_NoMultipleContentTypes { + get { + return ResourceManager.GetString("FileExtensionRegistry_NoMultipleContentTypes", resourceCulture); + } + } + } +} diff --git a/src/Core/Impl/ContentType/Strings.resx b/src/Core/Impl/ContentType/Strings.resx new file mode 100644 index 0000000..2591a20 --- /dev/null +++ b/src/Core/Impl/ContentType/Strings.resx @@ -0,0 +1,150 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="FileExtensionRegistry_NoMultipleContentTypes" xml:space="preserve"> + <value>{0} file name or extension cannot be associated with multiple content types.</value> + </data> + <data name="ContentTypeRegistry_UnknownTypeDisplayName" xml:space="preserve"> + <value>Unknown</value> + </data> + <data name="ContentTypeRegistry_CannotAddTypeWithEmptyTypeName" xml:space="preserve"> + <value>Cannot add content type with empty or undefined type name.</value> + </data> + <data name="ContentTypeRegistry_CannotProduceAnotherUnknownType" xml:space="preserve"> + <value>There can only be one "Unknown" content type defined. </value> + </data> + <data name="ContentTypeRegistry_ContentTypesCannotDeriveFromUnknown" xml:space="preserve"> + <value>Content type {0} cannot be added. No content type can be derive from the "Unknown" content type. </value> + </data> + <data name="ContentTypeRegistry_CausesCycles" xml:space="preserve"> + <value>The content type {0} leads to a cycle in its base types.</value> + </data> + <data name="ContentTypeRegistry_CannotAddExistentType" xml:space="preserve"> + <value>The content type {0} has already been defined.</value> + </data> + <data name="ContentTypeRegistry_CannotRemoveTheUnknownType" xml:space="preserve"> + <value>The "Unknown" content type cannot be removed.</value> + </data> + <data name="ContentTypeRegistry_CannotRemoveBaseType" xml:space="preserve"> + <value>The type {0} cannot be removed because it's used as base type for the content type {1}.</value> + </data> + <data name="ContentTypeRegistry_CannotRemoveTypeUsedByFileExtensions" xml:space="preserve"> + <value>The type {0} cannot be removed because it's associated with one or more file extensions.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/Language/Impl/StandardClassification/ClassificationFormatDefinitions.cs b/src/Language/Impl/StandardClassification/ClassificationFormatDefinitions.cs new file mode 100644 index 0000000..070dc76 --- /dev/null +++ b/src/Language/Impl/StandardClassification/ClassificationFormatDefinitions.cs @@ -0,0 +1,364 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Language.StandardClassification.Implementation +{ + using System.ComponentModel.Composition; + using System.Windows.Media; + + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Classification; + + // =========== Classification formats ================ + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekBackground)] + [Name("Peek Background")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class PeekBackgroundClassificationFormatDefinition : ClassificationFormatDefinition + { + public PeekBackgroundClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PeekBackground; + ForegroundCustomizable = false; + BackgroundColor = Color.FromRgb(0xF2, 0xF8, 0xFC); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekBackgroundUnfocused)] + [Name("Peek Background Unfocused")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class PeekBackgroundUnfocusedClassificationFormatDefinition : ClassificationFormatDefinition + { + public PeekBackgroundUnfocusedClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PeekBackgroundUnfocused; + ForegroundCustomizable = false; + BackgroundColor = Color.FromRgb(0xEB, 0xEB, 0xEB); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekHistorySelected)] + [Name("Peek History Selected")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class PeekHistorySelectedClassificationFormatDefinition : ClassificationFormatDefinition + { + public PeekHistorySelectedClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PeekBreadcrumbSelected; + ForegroundCustomizable = false; + BackgroundColor = Color.FromRgb(0, 0x7A, 0xCC); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekHistoryHovered)] + [Name("Peek History Hovered")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class PeekHistoryHoveredClassificationFormatDefinition : ClassificationFormatDefinition + { + public PeekHistoryHoveredClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PeekBreadcrumbHovered; + ForegroundCustomizable = false; + BackgroundColor = Color.FromRgb(0x1C, 0x97, 0xEA); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekFocusedBorder)] + [Name("Peek Focused Border")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class PeekFocusedBorderClassificationFormatDefinition : ClassificationFormatDefinition + { + public PeekFocusedBorderClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PeekFocusedBorder; + BackgroundCustomizable = false; + ForegroundColor = Color.FromRgb(0, 0x7A, 0xCC); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekLabelText)] + [Name("Peek Label Text")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class PeekLabelTextClassificationFormatDefinition: ClassificationFormatDefinition + { + public PeekLabelTextClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PeekLabelText; + BackgroundCustomizable = false; + ForegroundColor = Color.FromRgb(0, 0, 0); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekHighlightedText)] + [Name("Peek Highlighted Text")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class PeekHighlightedTextClassificationFormatDefinition : ClassificationFormatDefinition + { + public PeekHighlightedTextClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PeekHighlightedText; + ForegroundCustomizable = false; + BackgroundColor = Color.FromRgb(0xC1, 0xDE, 0xF1); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PeekHighlightedTextUnfocused)] + [Name("Peek Highlighted Text Unfocused")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class PeekHighlightedTextUnfocusedClassificationFormatDefinition : ClassificationFormatDefinition + { + public PeekHighlightedTextUnfocusedClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PeekHighlightedTextUnfocused; + ForegroundCustomizable = false; + BackgroundColor = Color.FromRgb(0xD2, 0xDA, 0xE0); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Comment)] + [Name("Comment")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "Excluded Code")] + internal class CommentClassificationFormatDefinition : ClassificationFormatDefinition + { + public CommentClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_Comment; + ForegroundColor = Color.FromRgb(0x00, 0x80, 0x00); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.ExcludedCode)] + [Name("Excluded Code")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "Identifier")] + internal class PreprocessorExcludeClassificationFormatDefinition : ClassificationFormatDefinition + { + public PreprocessorExcludeClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_ExcludedCode; + ForegroundColor = Colors.Gray; + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Identifier)] + [Name("Identifier")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "Keyword")] + internal class IdentifierClassificationFormatDefinition : ClassificationFormatDefinition + { + public IdentifierClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_Identifier; + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Keyword)] + [Name("Keyword")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "Literal")] + internal class KeywordClassificationFormatDefinition : ClassificationFormatDefinition + { + public KeywordClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_Keyword; + ForegroundColor = Color.FromRgb(0x00, 0x00, 0xFF); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.PreprocessorKeyword)] + [Name("Preprocessor Keyword")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "String")] + internal class PreprocessorKeywordClassificationFormatDefinition : ClassificationFormatDefinition + { + public PreprocessorKeywordClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_PreprocessorKeyword; + ForegroundColor = Color.FromRgb(0x80, 0x80, 0x80); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Operator)] + [Name("Operator")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "Preprocessor Keyword")] + internal class OperatorClassificationFormatDefinition : ClassificationFormatDefinition + { + public OperatorClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_Operator; + ForegroundColor = Color.FromRgb(0x00, 0x00, 0x00); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Literal)] + [Name("Literal")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "Number")] + internal class LiteralClassificationFormatDefinition : ClassificationFormatDefinition + { + public LiteralClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_Literal; + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.MarkupAttribute)] + [Name("Markup Attribute")] + [UserVisible(true)] + [Order(After = "Literal", Before = "Number")] + internal class MarkupAttributeFormatDefinition : ClassificationFormatDefinition + { + public MarkupAttributeFormatDefinition() + { + DisplayName = Strings.EditorFormat_MarkupAttribute; + ForegroundColor = Color.FromRgb(0xFF, 0x00, 0x00); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.MarkupAttributeValue)] + [Name("Markup Attribute Value")] + [UserVisible(true)] + [Order(After = "Literal", Before = "Number")] + internal class MarkupAttributeValueFormatDefinition : ClassificationFormatDefinition + { + public MarkupAttributeValueFormatDefinition() + { + DisplayName = Strings.EditorFormat_MarkupAttributeValue; + ForegroundColor = Color.FromRgb(0x00, 0x00, 0xFF); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.MarkupNode)] + [Name("Markup Node")] + [UserVisible(true)] + [Order(After = "Literal", Before = "Number")] + internal class MarkupNodeFormatDefinition : ClassificationFormatDefinition + { + public MarkupNodeFormatDefinition() + { + DisplayName = Strings.EditorFormat_MarkupNode; + ForegroundColor = Color.FromRgb(0x80, 0x00, 0x00); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.String)] + [Name("String")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "SymbolDefinitionClassificationFormat")] + internal class StringClassificationFormatDefinition : ClassificationFormatDefinition + { + public StringClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_String; + ForegroundColor = Color.FromRgb(0xA3, 0x15, 0x15); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Type)] + [Name("Type")] + [UserVisible(true)] + [Order(After = "String", Before = "Number")] + internal class TypeFormatDefinition : ClassificationFormatDefinition + { + public TypeFormatDefinition() + { + DisplayName = Strings.EditorFormat_Type; + ForegroundColor = Color.FromRgb(0x2B, 0x91, 0xAF); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.Number)] + [Name("Number")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "Operator")] + internal class StringCharacterNumericalClassificationFormatDefinition : ClassificationFormatDefinition + { + public StringCharacterNumericalClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_Number; + ForegroundColor = Color.FromRgb(0x00, 0x00, 0x00); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.SymbolDefinition)] + [Name("SymbolDefinitionClassificationFormat")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = "SymbolReferenceClassificationFormat")] + internal class SymbolDefinitionClassificationFormatDefinition : ClassificationFormatDefinition + { + public SymbolDefinitionClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_SymbolDefinition; + ForegroundColor = Color.FromRgb(0x2B, 0x91, 0xAF); + } + } + + [Export(typeof(EditorFormatDefinition))] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.SymbolReference)] + [Name("SymbolReferenceClassificationFormat")] + [UserVisible(true)] + [Order(After = LanguagePriority.NaturalLanguage, Before = LanguagePriority.FormalLanguage)] + internal class SymbolReferenceClassificationFormatDefinition : ClassificationFormatDefinition + { + public SymbolReferenceClassificationFormatDefinition() + { + DisplayName = Strings.EditorFormat_SymbolReference; + IsBold = true; + ForegroundColor = Color.FromRgb(0x00, 0x88, 0x00); + } + } + + [Export(typeof(EditorFormatDefinition))] + [Name(LanguagePriority.NaturalLanguage)] + [UserVisible(false)] + [Order(After = Priority.Default, Before = LanguagePriority.FormalLanguage)] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.NaturalLanguage)] + internal class NaturalLanguagePriorityClassificationFormatDefinition : ClassificationFormatDefinition + { + } + + [Export(typeof(EditorFormatDefinition))] + [Name(LanguagePriority.FormalLanguage)] + [UserVisible(false)] + [Order(After = LanguagePriority.NaturalLanguage, Before = Priority.High)] + [ClassificationType(ClassificationTypeNames = PredefinedClassificationTypeNames.FormalLanguage)] + internal class FormalLanguagePriorityClassificationFormatDefinition : ClassificationFormatDefinition + { + } +} diff --git a/src/Language/Impl/StandardClassification/StandardClassificationService.cs b/src/Language/Impl/StandardClassification/StandardClassificationService.cs new file mode 100644 index 0000000..893ad2f --- /dev/null +++ b/src/Language/Impl/StandardClassification/StandardClassificationService.cs @@ -0,0 +1,335 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Language.StandardClassification.Implementation +{ + using Microsoft.VisualStudio.Text.Classification; + using Microsoft.VisualStudio.Utilities; + using System.ComponentModel.Composition; + + /// <summary> + /// Helper service to get hold of standard classifications. + /// </summary> + [Export(typeof(IStandardClassificationService))] + internal sealed class StandardClassificationService : IStandardClassificationService + { + // =========== Classification types ================ + + [Export] + [Name(PredefinedClassificationTypeNames.NaturalLanguage)] + internal ClassificationTypeDefinition naturalLanguageClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition formalLanguageClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Comment)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition commentClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Identifier)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition identifierClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Keyword)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition keywordClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.WhiteSpace)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition whitespaceClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Operator)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition operatorClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Literal)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition literalClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.MarkupAttribute)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition markupAttributeClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.MarkupAttributeValue)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition markupAttributeValueClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.MarkupNode)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition markupNodeClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.String)] + [BaseDefinition(PredefinedClassificationTypeNames.Literal)] + internal ClassificationTypeDefinition stringClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Type)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition typeClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Character)] + [BaseDefinition(PredefinedClassificationTypeNames.Literal)] + internal ClassificationTypeDefinition characterClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Number)] + [BaseDefinition(PredefinedClassificationTypeNames.Literal)] + internal ClassificationTypeDefinition numberClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.Other)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition otherClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.ExcludedCode)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition excludedCodeClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.PreprocessorKeyword)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition preprocessorKeywordClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.SymbolDefinition)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition symbolDefinitionClassificationTypeDefinition; + + [Export] + [Name(PredefinedClassificationTypeNames.SymbolReference)] + [BaseDefinition(PredefinedClassificationTypeNames.FormalLanguage)] + internal ClassificationTypeDefinition symbolReferenceClassificationTypeDefinition; + + [Import] + internal IClassificationTypeRegistryService ClassificationTypeRegistry; + + #region IStandardClassificationService Members + + public IClassificationType CharacterLiteral + { + get + { + if (this.characterLiteral == null) + { + this.characterLiteral = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Character); + } + return this.characterLiteral; + } + } + IClassificationType characterLiteral; + + public IClassificationType Comment + { + get + { + if (this.comment == null) + { + this.comment = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Comment); + } + return this.comment; + } + } + IClassificationType comment; + + public IClassificationType FormalLanguage + { + get + { + if (this.formalLanguage == null) + { + this.formalLanguage = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.FormalLanguage); + } + return this.formalLanguage; + } + } + IClassificationType formalLanguage; + + public IClassificationType Identifier + { + get + { + if (this.identifier == null) + { + this.identifier = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Identifier); + } + return this.identifier; + } + } + IClassificationType identifier; + + public IClassificationType Keyword + { + get + { + if (this.keyword == null) + { + this.keyword = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Keyword); + } + return this.keyword; + } + } + IClassificationType keyword; + + public IClassificationType Literal + { + get + { + if (this.literal == null) + { + this.literal = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Literal); + } + return this.literal; + } + } + IClassificationType literal; + + public IClassificationType NaturalLanguage + { + get + { + if (this.naturalLanguage == null) + { + this.naturalLanguage = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.NaturalLanguage); + } + return this.naturalLanguage; + } + } + IClassificationType naturalLanguage; + + public IClassificationType NumberLiteral + { + get + { + if (this.numericalLiteral == null) + { + this.numericalLiteral = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Number); + } + return this.numericalLiteral; + } + } + IClassificationType numericalLiteral; + + public IClassificationType Operator + { + get + { + if (this.@operator == null) + { + this.@operator = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Operator); + } + return this.@operator; + } + } + IClassificationType @operator; + + public IClassificationType StringLiteral + { + get + { + if (this.stringLiteral == null) + { + this.stringLiteral = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.String); + } + return this.stringLiteral; + } + } + IClassificationType stringLiteral; + + public IClassificationType WhiteSpace + { + get + { + if (this.whiteSpace == null) + { + this.whiteSpace = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.WhiteSpace); + } + return this.whiteSpace; + } + } + IClassificationType whiteSpace; + + public IClassificationType Other + { + get + { + if (this.other == null) + { + this.other = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.Other); + } + return this.other; + } + } + IClassificationType other; + + public IClassificationType PreprocessorKeyword + { + get + { + if (this.preprocessorKeyword == null) + { + this.preprocessorKeyword = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.PreprocessorKeyword); + } + return this.preprocessorKeyword; + } + } + IClassificationType preprocessorKeyword; + + public IClassificationType ExcludedCode + { + get + { + if (this.excludedCode == null) + { + this.excludedCode = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.ExcludedCode); + } + return this.excludedCode; + } + } + IClassificationType excludedCode; + + public IClassificationType SymbolDefinition + { + get + { + if (this.symbolDefinition == null) + { + this.symbolDefinition = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.SymbolDefinition); + } + return this.symbolDefinition; + } + } + IClassificationType symbolDefinition; + + public IClassificationType SymbolReference + { + get + { + if (this.symbolReference == null) + { + this.symbolReference = ClassificationTypeRegistry.GetClassificationType(PredefinedClassificationTypeNames.SymbolReference); + } + return this.symbolReference; + } + } + IClassificationType symbolReference; + + #endregion + } +} diff --git a/src/Language/Impl/StandardClassification/Strings.Designer.cs b/src/Language/Impl/StandardClassification/Strings.Designer.cs new file mode 100644 index 0000000..f14fe7f --- /dev/null +++ b/src/Language/Impl/StandardClassification/Strings.Designer.cs @@ -0,0 +1,270 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.VisualStudio.Language.StandardClassification.Implementation { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Language.StandardClassification.Implementation.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to Comment. + /// </summary> + internal static string EditorFormat_Comment { + get { + return ResourceManager.GetString("EditorFormat_Comment", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Excluded Code. + /// </summary> + internal static string EditorFormat_ExcludedCode { + get { + return ResourceManager.GetString("EditorFormat_ExcludedCode", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Identifier. + /// </summary> + internal static string EditorFormat_Identifier { + get { + return ResourceManager.GetString("EditorFormat_Identifier", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Keyword. + /// </summary> + internal static string EditorFormat_Keyword { + get { + return ResourceManager.GetString("EditorFormat_Keyword", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Literal. + /// </summary> + internal static string EditorFormat_Literal { + get { + return ResourceManager.GetString("EditorFormat_Literal", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Markup Attribute. + /// </summary> + internal static string EditorFormat_MarkupAttribute { + get { + return ResourceManager.GetString("EditorFormat_MarkupAttribute", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Markup Attribute Value. + /// </summary> + internal static string EditorFormat_MarkupAttributeValue { + get { + return ResourceManager.GetString("EditorFormat_MarkupAttributeValue", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Markup Node. + /// </summary> + internal static string EditorFormat_MarkupNode { + get { + return ResourceManager.GetString("EditorFormat_MarkupNode", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Number. + /// </summary> + internal static string EditorFormat_Number { + get { + return ResourceManager.GetString("EditorFormat_Number", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Operator. + /// </summary> + internal static string EditorFormat_Operator { + get { + return ResourceManager.GetString("EditorFormat_Operator", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Peek Background. + /// </summary> + internal static string EditorFormat_PeekBackground { + get { + return ResourceManager.GetString("EditorFormat_PeekBackground", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Peek Background Unfocused. + /// </summary> + internal static string EditorFormat_PeekBackgroundUnfocused { + get { + return ResourceManager.GetString("EditorFormat_PeekBackgroundUnfocused", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Peek Breadcrumb Hovered. + /// </summary> + internal static string EditorFormat_PeekBreadcrumbHovered { + get { + return ResourceManager.GetString("EditorFormat_PeekBreadcrumbHovered", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Peek Breadcrumb Selected. + /// </summary> + internal static string EditorFormat_PeekBreadcrumbSelected { + get { + return ResourceManager.GetString("EditorFormat_PeekBreadcrumbSelected", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Peek Focused Border. + /// </summary> + internal static string EditorFormat_PeekFocusedBorder { + get { + return ResourceManager.GetString("EditorFormat_PeekFocusedBorder", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Peek Highlighted Text. + /// </summary> + internal static string EditorFormat_PeekHighlightedText { + get { + return ResourceManager.GetString("EditorFormat_PeekHighlightedText", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Peek Highlighted Text Unfocused. + /// </summary> + internal static string EditorFormat_PeekHighlightedTextUnfocused { + get { + return ResourceManager.GetString("EditorFormat_PeekHighlightedTextUnfocused", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Peek Label Text. + /// </summary> + internal static string EditorFormat_PeekLabelText { + get { + return ResourceManager.GetString("EditorFormat_PeekLabelText", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Preprocessor Keyword. + /// </summary> + internal static string EditorFormat_PreprocessorKeyword { + get { + return ResourceManager.GetString("EditorFormat_PreprocessorKeyword", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to String. + /// </summary> + internal static string EditorFormat_String { + get { + return ResourceManager.GetString("EditorFormat_String", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Symbol Definition. + /// </summary> + internal static string EditorFormat_SymbolDefinition { + get { + return ResourceManager.GetString("EditorFormat_SymbolDefinition", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Symbol Reference. + /// </summary> + internal static string EditorFormat_SymbolReference { + get { + return ResourceManager.GetString("EditorFormat_SymbolReference", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Type. + /// </summary> + internal static string EditorFormat_Type { + get { + return ResourceManager.GetString("EditorFormat_Type", resourceCulture); + } + } + } +} diff --git a/src/Language/Impl/StandardClassification/Strings.resx b/src/Language/Impl/StandardClassification/Strings.resx new file mode 100644 index 0000000..fd18ef6 --- /dev/null +++ b/src/Language/Impl/StandardClassification/Strings.resx @@ -0,0 +1,212 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="EditorFormat_Comment" xml:space="preserve"> + <value>Comment</value> + <comment>The display name of the 'Comment' editor format.</comment> + </data> + <data name="EditorFormat_ExcludedCode" xml:space="preserve"> + <value>Excluded Code</value> + <comment>The display name of the 'Excluded Code' editor format.</comment> + </data> + <data name="EditorFormat_Identifier" xml:space="preserve"> + <value>Identifier</value> + <comment>The display name of the 'Identifier' editor format.</comment> + </data> + <data name="EditorFormat_Keyword" xml:space="preserve"> + <value>Keyword</value> + <comment>The display name of the 'Keyword' editor format.</comment> + </data> + <data name="EditorFormat_PreprocessorKeyword" xml:space="preserve"> + <value>Preprocessor Keyword</value> + <comment>The display name of the 'Preprocessor Keyword' editor format.</comment> + </data> + <data name="EditorFormat_Operator" xml:space="preserve"> + <value>Operator</value> + <comment>The display name of the 'Operator' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_Literal" xml:space="preserve"> + <value>Literal</value> + <comment>The display name of the 'Literal' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_String" xml:space="preserve"> + <value>String</value> + <comment>The display name of the 'String' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_Number" xml:space="preserve"> + <value>Number</value> + <comment>The display name of the 'Number' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_SymbolDefinition" xml:space="preserve"> + <value>Symbol Definition</value> + <comment>The display name of the 'Symbol Definition' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_SymbolReference" xml:space="preserve"> + <value>Symbol Reference</value> + <comment>The display name of the 'Symbol Reference' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_PeekBackground" xml:space="preserve"> + <value>Peek Background</value> + <comment>The display name of the 'Peek Background' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_PeekFocusedBorder" xml:space="preserve"> + <value>Peek Focused Border</value> + <comment>The display name of the 'Peek Focused Background' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_PeekBreadcrumbSelected" xml:space="preserve"> + <value>Peek Breadcrumb Selected</value> + <comment>The display name of the 'Peek Breadcrumb Selected' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_PeekBackgroundUnfocused" xml:space="preserve"> + <value>Peek Background Unfocused</value> + <comment>The display name of the 'Peek Background Unfocused' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_PeekBreadcrumbHovered" xml:space="preserve"> + <value>Peek Breadcrumb Hovered</value> + <comment>The display name of the 'Peek Breadcrumb Hovered' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_PeekLabelText" xml:space="preserve"> + <value>Peek Label Text</value> + <comment>The display name of the 'Peek Label Text' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_PeekHighlightedText" xml:space="preserve"> + <value>Peek Highlighted Text</value> + <comment>The display name of the 'Peek Highlighted Text' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_PeekHighlightedTextUnfocused" xml:space="preserve"> + <value>Peek Highlighted Text Unfocused</value> + <comment>The display name of the 'Peek Highlighted Text Unfocused' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_MarkupNode" xml:space="preserve"> + <value>Markup Node</value> + <comment>The display name of the 'Markup Node' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_MarkupAttribute" xml:space="preserve"> + <value>Markup Attribute</value> + <comment>The display name of the 'Markup Attribute' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_MarkupAttributeValue" xml:space="preserve"> + <value>Markup Attribute Value</value> + <comment>The display name of the 'Markup Attribute Value' editor format. This shows up in Tools->Options->Fonts and Colors.</comment> + </data> + <data name="EditorFormat_Type" xml:space="preserve"> + <value>Type</value> + <comment>The display name of the 'Type' editor format. This shows up in Tools->Options->Fonts and Colors</comment> + </data> +</root>
\ No newline at end of file diff --git a/src/Microsoft.VisualStudio.Text.Implementation.csproj b/src/Microsoft.VisualStudio.Text.Implementation.csproj new file mode 100644 index 0000000..39876ed --- /dev/null +++ b/src/Microsoft.VisualStudio.Text.Implementation.csproj @@ -0,0 +1,32 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net46</TargetFramework> + <SignAssembly>true</SignAssembly> + <AssemblyOriginatorKeyFile>key.snk</AssemblyOriginatorKeyFile> + <DelaySign>false</DelaySign> + </PropertyGroup> + + <ItemGroup> + <Compile Remove="Core\Def\**\*" /> + <Compile Remove="Language\**" /> + <Compile Remove="Text\Def\**\*" /> + <EmbeddedResource Remove="Core\Def\**\*" /> + <EmbeddedResource Remove="Language\**\*" /> + <EmbeddedResource Remove="Text\Def\**\*" /> + <None Remove="Core\Def\**\*" /> + <None Remove="Language\**\*" /> + <None Remove="Text\Def\**\*" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> + <PackageReference Include="Microsoft.VisualStudio.CoreUtility" Version="15.3.1710.203" /> + <PackageReference Include="Microsoft.VisualStudio.Language.StandardClassification" Version="15.3.1710.203" /> + <PackageReference Include="Microsoft.VisualStudio.Text.Data" Version="15.3.1710.203" /> + <PackageReference Include="Microsoft.VisualStudio.Text.Internal" Version="15.3.1710.203" /> + <PackageReference Include="Microsoft.VisualStudio.Text.Logic" Version="15.3.1710.203" /> + <PackageReference Include="Microsoft.VisualStudio.Text.UI" Version="15.3.1710.203" /> + </ItemGroup> + +</Project>
\ No newline at end of file diff --git a/src/Text/Def/Internal/TextData/ExtensionMethods.cs b/src/Text/Def/Internal/TextData/ExtensionMethods.cs new file mode 100644 index 0000000..dd50540 --- /dev/null +++ b/src/Text/Def/Internal/TextData/ExtensionMethods.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + +namespace Microsoft.VisualStudio.Text +{ + public static class ExtensionMethods + { + /// <summary> + /// Find the index of the next non-whitespace character in a line. + /// </summary> + /// <param name="line">The line to search.</param> + /// <param name="startIndex">The index at which to begin the search, relative to the start of the line.</param> + /// <returns>The index, relative to the start of the line, of the first non-whitespace character whose index + /// is <paramref name="startIndex"/> or greater, or -1 if there are not any non-whitespace characters at that index or greater.</returns> + /// <exception cref="ArgumentOutOfRangeException"> if <paramref name="startIndex"/> is negative.</exception> + public static int IndexOfNextNonWhiteSpaceCharacter(this ITextSnapshotLine line, int startIndex) + { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException("start"); + } + + // Take advantage of performant [] operators on the ITextSnapshot + ITextSnapshot snapshot = line.Snapshot; + int snapshotPos = line.Start.Position; + for (int i = startIndex; i < line.Length; i++) + { + if (!char.IsWhiteSpace(snapshot[snapshotPos + i])) + { + return i; + } + } + + return -1; + } + + /// <summary> + /// Find the index of the previous non-whitespace character in a line. + /// </summary> + /// <param name="line">The line to search.</param> + /// <param name="startIndex">The index at which to begin the search, relative to the start of the line.</param> + /// <returns>The index, relative to the start of the line, of the first non-whitespace character whose index + /// is <paramref name="startIndex"/> or greater, or -1 if there are not any non-whitespace characters at that index or greater.</returns> + /// <exception cref="ArgumentOutOfRangeException"> if <paramref name="startIndex"/> is negative.</exception> + public static int IndexOfPreviousNonWhiteSpaceCharacter(this ITextSnapshotLine line, int startIndex) + { + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + + // Take advantage of performant [] operators on the ITextSnapshot + ITextSnapshot snapshot = line.Snapshot; + int lineStart = line.Start.Position; + for (int i = startIndex - 1; i >= 0; i--) + { + if (!char.IsWhiteSpace(snapshot[lineStart + i])) + { + return i; + } + } + + return -1; + } + } +} diff --git a/src/Text/Def/Internal/TextData/INonJoinableTaskTrackerInternal.cs b/src/Text/Def/Internal/TextData/INonJoinableTaskTrackerInternal.cs new file mode 100644 index 0000000..b4a0d46 --- /dev/null +++ b/src/Text/Def/Internal/TextData/INonJoinableTaskTrackerInternal.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Threading.Tasks; + +namespace Microsoft.VisualStudio.Text +{ + /// <summary> + /// Internal tracker for non-joinable tasks. Used to ensure that all pending tasks + /// have completed on editor host shutdown. + /// </summary> + /// <remarks>Methods of this interface can be called on any thread.</remarks> + public interface INonJoinableTaskTrackerInternal + { + void Register(Task task); + } +} diff --git a/src/Text/Def/Internal/TextData/IObjectTracker.cs b/src/Text/Def/Internal/TextData/IObjectTracker.cs new file mode 100644 index 0000000..7b4770b --- /dev/null +++ b/src/Text/Def/Internal/TextData/IObjectTracker.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// Describes a tracker of in-memory objects. + /// </summary> + public interface IObjectTracker + { + /// <summary> + /// Begins tracking an object. + /// </summary> + /// <param name="value">The <see cref="System.Object"/> to track.</param> + /// <param name="bucketName">The name of the bucket in which to track this <see cref="System.Object"/> reference.</param> + /// <remarks>Object trackers</remarks> + void TrackObject(object value, string bucketName); + } +} diff --git a/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs b/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs new file mode 100644 index 0000000..afe6de3 --- /dev/null +++ b/src/Text/Def/Internal/TextData/IStructureSpanningTreeManager.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Structure +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.UI.Adornments; + + /// <summary> + /// Defines the interface for the <see cref="IStructureSpanningTreeManager"/> which + /// provides information about the structural hierarchy of code in an <see cref="ITextView"/>. + /// </summary> + /// <remarks> + /// You can obtain an instance of this class via the <see cref="IStructureSpanningTreeService"/>. + /// </remarks> + public interface IStructureSpanningTreeManager + { + /// <summary> + /// Event that indicates that <see cref="SpanningTreeSnapshot"/> has been updated, + /// and that any method calls on this service will now return more up to date results. + /// </summary> + event EventHandler SpanningTreeChanged; + + /// <summary> + /// Gets an immutable instance of the most up to date current code structure. + /// </summary> + IStructureElement SpanningTreeSnapshot { get; } + + /// <summary> + /// Gets an enumerable of <see cref="IStructureElement"/>s that encapsulate the given <see cref="SnapshotPoint"/>. + /// </summary> + /// <remarks> + /// This method is intended as a projection-aware means to obtain language-service provided + /// structural context for a location such as the caret position, or a structure guide line. + /// </remarks> + /// <param name="point">A <see cref="SnapshotPoint"/> indicating the position of interest.</param> + /// <returns> + /// The elements within which <paramref name="point"/> is nested, in order, from outermost to innermost. + /// </returns> + IEnumerable<IStructureElement> GetElementsEncapsulatingPoint(SnapshotPoint point); + + /// <summary> + /// Gets an enumerable of <see cref="IStructureElement"/>s that intersect with the given + /// <see cref="SnapshotSpan"/>. + /// </summary> + /// <remarks> + /// This method is intended as a projection-aware means to obtain language-service provided + /// structural context for a span. + /// </remarks> + /// <param name="spans">The spans to collect elements from.</param> + /// <returns>The elements that intersect with the given span.</returns> + IEnumerable<IStructureElement> GetElementsIntersectingSpans(NormalizedSnapshotSpanCollection spans); + } +} diff --git a/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs b/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs new file mode 100644 index 0000000..1ad9c28 --- /dev/null +++ b/src/Text/Def/Internal/TextData/IStructureSpanningTreeService.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Structure +{ + using System; + using Microsoft.VisualStudio.Text.Editor; + + /// <summary> + /// Defines the interface for the <see cref="IStructureSpanningTreeService"/> which can be + /// used to obtain instances of the <see cref="IStructureSpanningTreeManager"/>, which + /// provides information about the structural hierarchy of code in an <see cref="ITextView"/>. + /// </summary> + /// <remarks> + /// This interface is a MEF component part and can be imported with a MEF import attribute. + /// <code> + /// [Import] + /// internal IStructureSpanningTreeService StructureSpanningTreeService { get; } + /// </code> + /// </remarks> + public interface IStructureSpanningTreeService + { + /// <summary> + /// Gets the singleton <see cref="IStructureSpanningTreeManager"/> for the specified view. + /// </summary> + /// <param name="textView">The view to get the structure manager for.</param> + /// <exception cref="InvalidOperationException">Throw if not called from the UI thread.</exception> + /// <returns>The singleton instance of <see cref="IStructureSpanningTreeManager"/> for the view.</returns> + IStructureSpanningTreeManager GetManager(ITextView textView); + } +} diff --git a/src/Text/Def/Internal/TextData/ITextBufferFactoryService2.cs b/src/Text/Def/Internal/TextData/ITextBufferFactoryService2.cs new file mode 100644 index 0000000..f9789e3 --- /dev/null +++ b/src/Text/Def/Internal/TextData/ITextBufferFactoryService2.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text +{ + using System; + using System.IO; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// The factory service for ordinary TextBuffers. + /// </summary> + /// <remarks><para>This is a MEF Component, and should be imported as follows: + /// <code> + /// [Import] + /// ITextBufferFactoryService factory = null; + /// </code> + /// </para> + /// <para>Any <see cref="ITextBufferFactoryService"/> will be upcastable to an <see cref="ITextBufferFactoryService2"/>.</para> + /// </remarks> + public interface ITextBufferFactoryService2 : ITextBufferFactoryService + { + /// <summary> + /// Creates an <see cref="ITextBuffer"/> with the specified <see cref="IContentType"/> and populates it + /// with the given text contained in <paramref name="span"/>. + /// </summary> + /// <param name="span">The initial text to add.</param> + /// <param name="contentType">The <see cref="IContentType"/> for the new <see cref="ITextBuffer"/>.</param> + /// <returns> + /// A <see cref="ITextBuffer"/> object with the given text and <see cref="IContentType"/>. + /// </returns> + /// <exception cref="ArgumentNullException">Either <paramref name="span"/> or <paramref name="contentType"/> is null.</exception> + ITextBuffer CreateTextBuffer(SnapshotSpan span, IContentType contentType); + + /// <summary> + /// Creates an <see cref="ITextBuffer"/> with the given <paramref name="contentType"/> and populates it by + /// reading data from the specified TextReader. + /// </summary> + /// <param name="reader">The TextReader from which to read.</param> + /// <param name="contentType">The <paramref name="contentType"/> for the text contained in the new <see cref="ITextBuffer"/></param> + /// <param name="length">The length of the file backing the text reader, if known; otherwise -1.</param> + /// <param name="traceId">An optional identifier used in debug tracing.</param> + /// <returns> + /// An <see cref="ITextBuffer"/> object with the given TextReader and <paramref name="contentType"/>. + /// </returns> + /// <exception cref="ArgumentNullException"><paramref name="reader"/> is null.</exception> + /// <exception cref="ArgumentNullException"><paramref name="contentType"/> is null.</exception> + /// <remarks> + /// <para>The <paramref name="reader"/> is not closed by this operation.</para> + /// <para>The <paramref name="length"/> is used to help select a storage strategy for the text buffer.</para> + /// </remarks> + ITextBuffer CreateTextBuffer(TextReader reader, IContentType contentType, long length = -1, string traceId = ""); + } +} diff --git a/src/Text/Def/Internal/TextData/ITextImageFactoryService2.cs b/src/Text/Def/Internal/TextData/ITextImageFactoryService2.cs new file mode 100644 index 0000000..8e91757 --- /dev/null +++ b/src/Text/Def/Internal/TextData/ITextImageFactoryService2.cs @@ -0,0 +1,36 @@ +// +// 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 +{ + using System; + using System.IO; + using System.IO.MemoryMappedFiles; + + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// The factory service for creating <see cref="ITextImage"/>s. + /// </summary> + /// <remarks>This is a MEF Component, and should be imported as follows: + /// <code> + /// [Import] + /// ITextImageFactoryService factory = null; + /// </code> + /// </remarks> + public interface ITextImageFactoryService2 : ITextImageFactoryService + { + /// <summary> + /// Create an <see cref="ITextImage"/> from a memory mapped file. + /// </summary> + /// <param name="source">A utf-16 encoded image of the file.</param> + /// <returns>An <see cref="ITextImage"/> that is mapped to the contents of <paramref name="source"/>.</returns> + /// <remarks> + /// <para><paramref name="source"/> must be encoded as utf-16.</para> + /// <para>Any <see cref="ITextImage"/> created from <paramref name="source"/> be invalidated if <paramref name="source"/> is either + /// modified or disposed of.</para> + /// </remarks> + ITextImage CreateTextImage(MemoryMappedFile source); + } +} diff --git a/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs b/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs new file mode 100644 index 0000000..64c0c0d --- /dev/null +++ b/src/Text/Def/Internal/TextData/JoinableTaskHelper.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Threading; + +namespace Microsoft.VisualStudio.Utilities +{ + /// <summary> + /// A helper for managing JoinableTasks. + /// </summary> + public class JoinableTaskHelper + { + public readonly JoinableTaskContext Context; + public readonly JoinableTaskCollection Collection; + public readonly JoinableTaskFactory Factory; + + public JoinableTaskHelper(JoinableTaskContext context) + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + this.Context = context; + this.Collection = context.CreateCollection(); + this.Factory = context.CreateFactory(this.Collection); + } + + public JoinableTask RunOnUIThread(Action action, bool forceTaskSwitch = true) + { + using (this.Context.SuppressRelevance()) + { + return this.Factory.RunAsync(async delegate + { + if (forceTaskSwitch && this.Context.IsOnMainThread) + { + await Task.Yield(); + } + + await this.Factory.SwitchToMainThreadAsync(); + action(); + }); + } + } + + public JoinableTask<T> RunOnUIThread<T>(Func<T> function, bool forceTaskSwitch = true) + { + using (this.Context.SuppressRelevance()) + { + return this.Factory.RunAsync(async delegate + { + if (forceTaskSwitch && this.Context.IsOnMainThread) + { + await Task.Yield(); + } + + await this.Factory.SwitchToMainThreadAsync(); + return function(); + }); + } + } + + public async Task DisposeAsync() + { + await this.Collection.JoinTillEmptyAsync(); + } + + public void Dispose() + { + this.Context.Factory.Run(async delegate { // Not this.Factory + await this.DisposeAsync(); + }); + } + } +} + diff --git a/src/Text/Def/Internal/TextData/LazyObservableCollection.cs b/src/Text/Def/Internal/TextData/LazyObservableCollection.cs new file mode 100644 index 0000000..2c4849f --- /dev/null +++ b/src/Text/Def/Internal/TextData/LazyObservableCollection.cs @@ -0,0 +1,680 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + public delegate TWrapper WrapperCreator<TData, TWrapper>(TData underlyingData, int index); + + /// <summary> + /// A virtualized data collection that can be used to create wrapper objects on-demand. + /// </summary> + public class LazyObservableCollection<TData, TWrapper> : + IList<TWrapper>, + IList, + INotifyCollectionChanged, + INotifyPropertyChanged, + IDisposable + where TWrapper : class + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region Private Fields + + private bool _disposed = false; + private TData _underlyingDataObject; + private int _count; + private WrapperCreator<TData, TWrapper> _wrapperCreator; + private Dictionary<int, TWrapper> _wrapperDictionary = new Dictionary<int, TWrapper>(); + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region Constructors + + /// <summary> + /// Constructs a virtualized list over an underlying data object. + /// </summary> + /// <param name="underlyingDataObject">The underlying object over which wrappers will be created.</param> + /// <param name="dataObjectCount"> + /// The number of "items" in the underlying object. Also the number of wrappers to be created. + /// </param> + /// <param name="wrapperCreator"> + /// A delegate that will create wrapper objects given an index into the underlying object. + /// </param> + public LazyObservableCollection + ( + TData underlyingDataObject, + int dataObjectCount, + WrapperCreator<TData, TWrapper> wrapperCreator + ) + { + _underlyingDataObject = underlyingDataObject; + _count = dataObjectCount; + _wrapperCreator = wrapperCreator; + this.SubscribeToEvents(); + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region IList<TWrapper> Members + + /// <summary> + /// Determines the index of a specific item in the <see cref="LazyObservableCollection{TData, TWrapper}"/>. + /// </summary> + /// <param name="item">The object to locate in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.</param> + /// <returns>The index of item if found in the list; otherwise, -1.</returns> + public int IndexOf(TWrapper item) + { + if (_disposed) + return -1; + + // We've got a problem here. The caller is asking us to find the index of a wrapper. This could be a wrapper around + // any underlying data object. We have to assume that the caller is asking about a wrapper that we previously gave + // to them. + if (_wrapperDictionary.ContainsValue(item)) + { + Dictionary<int, TWrapper>.KeyCollection tempKeys = _wrapperDictionary.Keys; + + foreach (int key in tempKeys) + { + if (item == _wrapperDictionary[key]) + { + return key; + } + } + } + + return -1; + } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public void Insert(int index, TWrapper item) + { + throw new InvalidOperationException("The collection is read-only."); + } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public void RemoveAt(int index) + { + throw new InvalidOperationException("The collection is read-only."); + } + + /// <summary> + /// Gets the element at the specified index. Although the set accessor is defined, it will throw an + /// <see cref="InvalidOperationException"/> when called, as the <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + /// <param name="index">The zero-based index of the element to get or set.</param> + /// <returns>The element at the specified index.</returns> + public TWrapper this[int index] + { + get + { + return this.GetWrapper(index); + } + set + { + throw new InvalidOperationException("The collection is read-only."); + } + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region IList Members + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public int Add(object value) + { + throw new InvalidOperationException("The collection is read-only."); + } + + /// <summary> + /// Determines whether the <see cref="LazyObservableCollection{TData, TWrapper}"/> contains a specific value. + /// </summary> + /// <param name="value">The <see cref="System.Object"/> to locate in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.</param> + /// <returns> + /// true if the <see cref="System.Object"/> is found in the <see cref="LazyObservableCollection{TData, TWrapper}"/>; otherwise, false. + /// </returns> + public bool Contains(object value) + { + TWrapper wrapperObj = value as TWrapper; + if ((value != null) && (wrapperObj == null)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "value is not of type {0}", typeof(TWrapper).FullName), "value"); + } + + return this.Contains(wrapperObj); + } + + /// <summary> + /// Determines the index of a specific item in the <see cref="LazyObservableCollection{TData, TWrapper}"/>. + /// </summary> + /// <param name="value">The <see cref="System.Object"/> to locate in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.</param> + /// <returns>The index of value if found in the list; otherwise, -1.</returns> + public int IndexOf(object value) + { + TWrapper wrapperObj = value as TWrapper; + if ((value != null) && (wrapperObj == null)) + { + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "value is not of type {0}", typeof(TWrapper).FullName), "value"); + } + + return this.IndexOf(wrapperObj); + } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public void Insert(int index, object value) + { + throw new InvalidOperationException("The collection is read-only."); + } + + /// <summary> + /// Gets a value indicating whether the <see cref="InvalidOperationException"/> has a fixed size. + /// </summary> + public bool IsFixedSize + { + get { return true; } + } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public void Remove(object value) + { + throw new InvalidOperationException("The collection is read-only."); + } + + /// <summary> + /// Gets the element at the specified index. Although the set accessor is defined, it will throw an + /// <see cref="InvalidOperationException"/> when called, as the <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + /// <param name="index">The zero-based index of the element to get or set.</param> + /// <returns>The element at the specified index.</returns> + object IList.this[int index] + { + get + { + return ((IList<TWrapper>)this)[index]; + } + set + { + throw new InvalidOperationException("The collection is read-only."); + } + } + + /// <summary> + /// Copies the elements of the <see cref="LazyObservableCollection{TData, TWrapper}"/> to an <see cref="System.Array"/>, starting at a + /// particular <see cref="System.Array"/> index. + /// </summary> + /// <param name="array"> + /// The one-dimensional <see cref="System.Array"/> that is the destination of the elements copied from + /// the <see cref="LazyObservableCollection{TData, TWrapper}"/>. The <see cref="System.Array"/> must have zero-based indexing. + /// </param> + /// <param name="index">The zero-based index in array at which copying begins.</param> + public void CopyTo(Array array, int index) + { + if ((array.Length - index) < this.Count) + { + throw new ArgumentException("Array not big enough", "array"); + } + + int i = index; + foreach (TWrapper wrapper in this) + { + array.SetValue(wrapper, i); + i++; + } + } + + /// <summary> + /// Gets a value indicating whether access to the <see cref="LazyObservableCollection{TData, TWrapper}"/> is synchronized (thread safe). + /// </summary> + public bool IsSynchronized + { + get { return false; } + } + + /// <summary> + /// Gets an object that can be used to synchronize access to the <see cref="LazyObservableCollection{TData, TWrapper}"/>. + /// </summary> + public object SyncRoot + { + get + { + if (_disposed) + return null; + + return ((ICollection)_wrapperDictionary).SyncRoot; + } + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region ICollection<TWrapper> Members + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public void Add(TWrapper item) + { + throw new InvalidOperationException("The collection is read-only."); + } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public void Clear() + { + throw new InvalidOperationException("The collection is read-only."); + } + + /// <summary> + /// Determines whether the <see cref="LazyObservableCollection{TData, TWrapper}"/> contains a specific value. + /// </summary> + /// <param name="item">The value to locate in the <see cref="LazyObservableCollection{TData, TWrapper}"/>.</param> + /// <returns> + /// true if the value is found in the <see cref="LazyObservableCollection{TData, TWrapper}"/>; otherwise, false. + /// </returns> + public bool Contains(TWrapper item) + { + if (_disposed) + return false; + + return _wrapperDictionary.ContainsValue(item); + } + + /// <summary> + /// Copies the elements of the <see cref="LazyObservableCollection{TData, TWrapper}"/> to an array, starting at a particular array index. + /// </summary> + /// <param name="array"> + /// The one-dimensional array that is the destination of the elements copied from the + /// <see cref="LazyObservableCollection{TData, TWrapper}"/>. The array must have zero-based indexing. + /// </param> + /// <param name="arrayIndex">The zero-based index in array at which copying begins.</param> + public void CopyTo(TWrapper[] array, int arrayIndex) + { + if (_disposed) + return; + + // Before we do the copy, we have to ensure that the entire underlying collection is wrapped. That way, the caller + // will get a full copy of the list, not just the items we've realized so-far. + this.EnsureEntirelyWrapped(); + _wrapperDictionary.Values.CopyTo(array, arrayIndex); + } + + /// <summary> + /// Gets the number of elements contained in the <see cref="LazyObservableCollection{TData, TWrapper}"/>. + /// </summary> + public int Count + { + get { return _count; } + } + + /// <summary> + /// Gets a value indicating whether the <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public bool IsReadOnly + { + get { return true; } + } + + /// <summary> + /// Throws an <see cref="InvalidOperationException"/> when called. The <see cref="LazyObservableCollection{TData, TWrapper}"/> is read-only. + /// </summary> + public bool Remove(TWrapper item) + { + throw new InvalidOperationException("The collection is read-only."); + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region IEnumerable<TWrapper> Members + + /// <summary> + /// Returns an enumerator that iterates through the collection. + /// </summary> + /// <returns> + /// A <see cref="System.Collections.Generic.IEnumerator{T}"/> that can be used to iterate through the collection. + /// </returns> + public IEnumerator<TWrapper> GetEnumerator() + { + return new LazyObservableCollectionEnumerator(this); + } + + /// <summary> + /// Returns an enumerator that iterates through the collection. + /// </summary> + /// <returns> + /// A <see cref="System.Collections.IEnumerator"/> that can be used to iterate through the collection. + /// </returns> + IEnumerator IEnumerable.GetEnumerator() + { + return new LazyObservableCollectionEnumerator(this); + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region INotifyCollectionChanged Members + + /// <summary> + /// Raised when the set of items in the <see cref="LazyObservableCollection{TData, TWrapper}"/> changes. + /// </summary> + public event NotifyCollectionChangedEventHandler CollectionChanged; + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region INotifyPropertyChanged Members + + public event PropertyChangedEventHandler PropertyChanged; + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region IDisposable Members + + /// <summary> + /// Disposes and releases all wrappers created. Also releases all references to the underlying object + /// </summary> + public void Dispose() + { + if (_disposed) return; + _disposed = true; + + this.Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region Protected Surface + + protected virtual void Dispose(bool disposeManaged) + { + if (disposeManaged) + { + this.UnsubscribeFromEvents(); + + // Call dispose on the wrapper items and clean up the list + DisposeItems(_wrapperDictionary); + + _wrapperDictionary = null; + + // Stop holding-on to the wrapper creator delegate + _wrapperCreator = null; + } + + // Stop holding-on to the underlying collection (might be a native COM object, so do this regardless) + _underlyingDataObject = default(TData); + _count = 0; + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region Public Surface + + /// <summary> + /// Gets a wrapper object over the underlying object for the specified index. + /// </summary> + /// <param name="index">The index for which to obtain a wrapper</param> + /// <returns>A valid wrapper object for the specified index.</returns> + public TWrapper GetWrapper(int index) + { + if (_disposed) + throw new ObjectDisposedException("LazyObservableCollection"); + + TWrapper wrapper; + if (_wrapperDictionary.TryGetValue(index, out wrapper)) + { + return wrapper; + } + + // We don't yet have a wrapper for this data item. Go ahead and create one. + wrapper = _wrapperCreator(_underlyingDataObject, index); + _wrapperDictionary[index] = wrapper; + return wrapper; + } + + /// <summary> + /// Sets the underlying data object over which this <see cref="LazyObservableCollection{TData, TWrapper}"/> generates + /// wrapper objects. + /// </summary> + /// <param name="newDataObject">The underlying data object over which to generate wrappers.</param> + /// <param name="count">The number of items in the underlying object</param> + public void SetUnderlyingDataObject(TData newDataObject, int count) + { + // If we're subscribed to INotifyCollectionChanged on the old underlying data object, stop listening. + this.UnsubscribeFromEvents(); + + // Reset the underlying object and listen to the new INotifyCollectionChanged, if it exists. + _underlyingDataObject = newDataObject; + _count = count; + this.SubscribeToEvents(); + + // Get rid of all of our old wrappers + this.NotifyUnderlyingObjectChanged(); + } + + /// <summary> + /// Notifies the <see cref="LazyObservableCollection{TData, TWrapper}"/> that the underlying object over which the + /// <see cref="LazyObservableCollection{TData, TWrapper}"/> is based has changed. + /// </summary> + /// <remarks> + /// When the underlying object changes, the <see cref="LazyObservableCollection{TData, TWrapper}"/> resets its wrapper + /// collection and raises its INotifyCollectionChanged.CollectionChanged event. The next time wrappers are requested from + /// the <see cref="LazyObservableCollection{TData, TWrapper}"/>, the wrappers will be re-generated. + /// </remarks> + public void NotifyUnderlyingObjectChanged() + { + if (_disposed) + return; + + // Since the underlying collection changed, we can no longer be sure that our wrappers correspond to the right + // underlying data objects. Therefore, we'll reset our collection entirely. + + // 1. Keep track of old collection of items + Dictionary<int, TWrapper> oldItems = _wrapperDictionary; + + // 2. Recreate the collection of items and update count + IList underlyingList = _underlyingDataObject as IList; + if (underlyingList != null) + { + _count = underlyingList.Count; + } + // we want the dictionary to start small, as its future size is little related + // to its prior size (or the size of the underlying list). + _wrapperDictionary = new Dictionary<int, TWrapper>(); + + // 3. Notify listeners of the change + this.RaiseCollectionChanged(); + + // 4. Call dispose on the old wrapper items + DisposeItems(oldItems); + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region Event Handlers + + private void OnUnderlyingCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + this.NotifyUnderlyingObjectChanged(); + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region Private Implementation + + private static void DisposeItems(Dictionary<int, TWrapper> collection) + { + foreach (TWrapper wrapper in collection.Values) + { + IDisposable disposableWrapper = wrapper as IDisposable; + if (disposableWrapper != null) + { + disposableWrapper.Dispose(); + } + } + } + + private void EnsureEntirelyWrapped() + { + for (int i = 0; i < this.Count; i++) + { + this.GetWrapper(i); + } + } + + private void RaiseCollectionChanged() + { + NotifyCollectionChangedEventHandler tempHandler = this.CollectionChanged; + if (tempHandler != null) + { + tempHandler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + // Also notify consumers of the change to the 'Count' property. + this.RaisePropertyChanged("Count"); + } + + private void RaisePropertyChanged(string propertyName) + { + PropertyChangedEventHandler tempHandler = this.PropertyChanged; + if (tempHandler != null) + { + tempHandler(this, new PropertyChangedEventArgs(propertyName)); + } + } + + private void SubscribeToEvents() + { + INotifyCollectionChanged changingUnderlyingColleciton = _underlyingDataObject as INotifyCollectionChanged; + if (changingUnderlyingColleciton != null) + { + changingUnderlyingColleciton.CollectionChanged += this.OnUnderlyingCollection_CollectionChanged; + } + } + + private void UnsubscribeFromEvents() + { + INotifyCollectionChanged changingUnderlyingColleciton = _underlyingDataObject as INotifyCollectionChanged; + if (changingUnderlyingColleciton != null) + { + changingUnderlyingColleciton.CollectionChanged -= this.OnUnderlyingCollection_CollectionChanged; + } + } + + #endregion + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #region Nested Classes + + private class LazyObservableCollectionEnumerator : IEnumerator<TWrapper> + { + private int _currentIndex = -1; + private LazyObservableCollection<TData, TWrapper> _collection; + + /// <summary> + /// Creates an instance of the <see cref="LazyObservableCollectionEnumerator"/> class. + /// </summary> + /// <param name="collection"></param> + public LazyObservableCollectionEnumerator(LazyObservableCollection<TData, TWrapper> collection) + { + _collection = collection; + } + + /// <summary> + /// Gets the element in the collection at the current position of the enumerator. + /// </summary> + public TWrapper Current + { + get + { + if (_currentIndex < 0) + { + throw new InvalidOperationException("Enumerator position before first element in the collection"); + } + return _collection[_currentIndex]; + } + } + + /// <summary> + /// Gets the element in the collection at the current position of the enumerator. + /// </summary> + object IEnumerator.Current + { + get + { + if (_currentIndex < 0) + { + throw new InvalidOperationException("Enumerator position before first element in the collection"); + } + return _collection[_currentIndex]; + } + } + + /// <summary> + /// Releases all references to the collection being iterated. + /// </summary> + public void Dispose() + { + _collection = null; + GC.SuppressFinalize(this); + } + + /// <summary> + /// Advances the enumerator to the next element of the collection. + /// </summary> + /// <returns> + /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of + /// the collection. + /// </returns> + public bool MoveNext() + { + if (_currentIndex < _collection.Count - 1) + { + _currentIndex++; + return true; + } + + return false; + } + + /// <summary> + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// </summary> + public void Reset() + { + _currentIndex = -1; + } + } + + #endregion + } +} diff --git a/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs b/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs new file mode 100644 index 0000000..4cb900c --- /dev/null +++ b/src/Text/Def/Internal/TextData/TextBufferOperationHelpers.cs @@ -0,0 +1,95 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// + +using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + +namespace Microsoft.VisualStudio.Text +{ + public class TextBufferOperationHelpers + { + /// <summary> + /// Checks if the given <see cref="ITextSnapshotLine"/> has any non-whitespace characters + /// </summary> + /// <param name="line">The <see cref="ITextSnapshotLine"/> on which the check is performed</param> + /// <returns>True if the <see cref="ITextSnapshotLine"/> contains any non-whitespace characters</returns> + public static bool HasAnyNonWhitespaceCharacters(ITextSnapshotLine line) + { + return line.IndexOfPreviousNonWhiteSpaceCharacter(line.End.Position - line.Start.Position ) != -1; + } + + /// <summary> + /// For a given <see cref="ITextSnapshotLine"/> gets the new line character to be inserted to the line based on + /// either the given line, or the second last line or the default new line charcter provided by <see cref="IEditorOptions"/> + /// </summary> + /// <param name="line">The <see cref="ITextSnapshotLine"/> for whcih the new line character is to be decied for</param> + /// <param name="editorOptions">The current set of <see cref="IEditorOptions"/> applicable for the given <see cref="ITextSnapshotLine"/></param> + /// <returns>The new line character to be inserted</returns> + public static string GetNewLineCharacterToInsert(ITextSnapshotLine line, IEditorOptions editorOptions) + { + string lineBreak = null; + var snapshot = line.Snapshot; + + if (editorOptions.GetReplicateNewLineCharacter()) + { + if (line.LineBreakLength > 0) + { + // use the same line ending as the current line + lineBreak = line.GetLineBreakText(); + } + else + { + if (snapshot.LineCount > 1) + { + // use the same line ending as the penultimate line in the buffer + lineBreak = snapshot.GetLineFromLineNumber(snapshot.LineCount - 2).GetLineBreakText(); + } + } + } + string textToInsert = lineBreak ?? editorOptions.GetNewLineCharacter(); + return textToInsert; + } + + /// <summary> + /// Inserts a final new line for the given <see cref="ITextBuffer"/> based on + /// whether the option to insert it is enabled in the current set of <see cref="IEditorOptions"/> applicable to the buffer + /// </summary> + /// <param name="buffer">The <see cref="ITextBuffer"/> in which the final new line has to be inserted in</param> + /// <param name="editorOptions">The current set of <see cref="IEditorOptions"/> applicable to the buffer</param> + /// <returns>Whether the operation on the buffer succeded or not</returns> + public static bool TryInsertFinalNewLine(ITextBuffer buffer, IEditorOptions editorOptions) + { + var currentSnapshot = buffer.CurrentSnapshot; + var lineCount = currentSnapshot.LineCount; + var lastLine = currentSnapshot.GetLineFromLineNumber(lineCount - 1); + ITextSnapshot changedSnapshot = null; + + if (lastLine.Start.Position != lastLine.EndIncludingLineBreak.Position) // Check if final new line is not already present + { + var IsTrimTrailingWhitespacesSetExplicitlyToFalse = editorOptions.IsOptionDefined<bool>(DefaultOptions.TrimTrailingWhiteSpaceOptionId, true) && !editorOptions.GetOptionValue<bool>(DefaultOptions.TrimTrailingWhiteSpaceOptionId); + + if (!IsTrimTrailingWhitespacesSetExplicitlyToFalse && !HasAnyNonWhitespaceCharacters(lastLine)) // Last Line contains only of whitespace and trim trailing whitespaces is set to false + { + var spanToDelete = lastLine.ExtentIncludingLineBreak; + changedSnapshot = buffer.Delete(spanToDelete); + } + else // Non empty last line or empty last line with trim trailing whitespaces set to false. Insert a new line after the current line + { + string lineBreakToInsert = GetNewLineCharacterToInsert(lastLine, editorOptions); + var positionToInsertNewLine = lastLine.End.Position; + changedSnapshot = buffer.Insert(positionToInsertNewLine, lineBreakToInsert); + } + // Edits were successfull + if (changedSnapshot != null && currentSnapshot != changedSnapshot) + return true; + return false; + } + return true; + } + } +} diff --git a/src/Text/Def/Internal/TextData/TrackingSpanTree.cs b/src/Text/Def/Internal/TextData/TrackingSpanTree.cs new file mode 100644 index 0000000..8a7b195 --- /dev/null +++ b/src/Text/Def/Internal/TextData/TrackingSpanTree.cs @@ -0,0 +1,618 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Utilities +{ + using Microsoft.VisualStudio.Text; + + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + + /// <summary> + /// Holds a well-formed tree of tracking spans and correlated items, for tracking the position and movement + /// of spans over time. Allows for efficient addition, removal, and searching over the tree, with methods for + /// searching for intersection and containment over both <see cref="SnapshotSpan"/> and <see cref="NormalizedSnapshotSpanCollection"/> + /// arguments. The search results are returned pre-order (so parents before children, children closest to the start of the buffer first). + /// </summary> + /// <remarks> + /// <para> + /// Well-formed means the relationship between all spans is either non-overlapping (siblings) or + /// containment (parent-child), so there can be no partially overlapping spans. + /// </para> + /// <para> + /// The only tracking mode that can be safely used is <see cref="SpanTrackingMode.EdgeExclusive"/>, as the other tracking modes + /// can result in overlapping spans as the buffer changes. + /// </para> + /// </remarks> + /// <typeparam name="T">The type of object that each span correlates to.</typeparam> + /// <comment>This is used by the outlining manager to store collapsed regions and the outlining shims to store all hidden region adapters.</comment> + public sealed class TrackingSpanTree<T> + { + public TrackingSpanNode<T> Root { get; private set; } + public ITextBuffer Buffer { get; private set; } + + public int Count { get; private set; } + + private int advanceVersion = 0; + + /// <summary> + /// Create a tracking span tree for the given buffer. + /// </summary> + /// <param name="buffer">The buffer that all the spans in this tree are in.</param> + /// <param name="keepTrackingCurrent">The tree should not allow tracking spans to point + /// to old versions, at the expense of walking the tree on every text change.</param> + public TrackingSpanTree(ITextBuffer buffer, bool keepTrackingCurrent) + { + if (buffer == null) + throw new ArgumentNullException("buffer"); + + Buffer = buffer; + Count = 0; + + // Leave the tracking span parameter as null, since it is + // never used (assumed to always be the entire buffer) + Root = new TrackingSpanNode<T>(default(T), null); + + if (keepTrackingCurrent) + { + buffer.Changed += OnBufferChanged; + } + } + + /// <summary> + /// Try to add an item to the tree with the given tracking span. + /// </summary> + /// <param name="item">The item to add to the tree.</param> + /// <param name="trackingSpan">The tracking span it is associated with.</param> + /// <returns>The newly added node, if the item was successfully added; <c>null</c> if adding the item to the tree would + /// violate the well-formedness of the tree.</returns> + /// <exception cref="ArgumentNullException">If <paramref name="trackingSpan"/> is <c>null</c>.</exception> + /// <exception cref="ArgumentException">If the tracking mode of <paramref name="trackingSpan"/> is not <see cref="SpanTrackingMode.EdgeExclusive"/>.</exception> + public TrackingSpanNode<T> TryAddItem(T item, ITrackingSpan trackingSpan) + { + if (trackingSpan == null) + throw new ArgumentNullException("trackingSpan"); + + if (trackingSpan.TrackingMode != SpanTrackingMode.EdgeExclusive) + throw new ArgumentException("The tracking mode of the given tracking span must be SpanTrackingMode.EdgeExclusive", "trackingSpan"); + + SnapshotSpan spanToAdd = trackingSpan.GetSpan(Buffer.CurrentSnapshot); + TrackingSpanNode<T> node = new TrackingSpanNode<T>(item, trackingSpan); + + var newNode = TryAddNodeToRoot(node, spanToAdd, Root); + if (newNode != null) + Count++; + + return newNode; + } + + /// <summary> + /// Remove an item from the tree. + /// </summary> + /// <param name="item">The item to remove.</param> + /// <param name="trackingSpan">The span the item was located at.</param> + /// <returns><c>true</c> if the item was removed, <c>false</c> if it wasn't found.</returns> + public bool RemoveItem(T item, ITrackingSpan trackingSpan) + { + if (trackingSpan == null) + throw new ArgumentNullException("trackingSpan"); + + SnapshotSpan spanToRemove = trackingSpan.GetSpan(Buffer.CurrentSnapshot); + + if (RemoveItemFromRoot(item, spanToRemove, Root)) + { + Count--; + return true; + } + + return false; + } + + /// <summary> + /// Remove all nodes from the tree. + /// </summary> + public void Clear() + { + Root.Children.Clear(); + Count = 0; + } + + /// <summary> + /// Find nodes that intersect the given span. + /// </summary> + /// <param name="span">The span to search.</param> + /// <returns>Nodes that intersect the given span.</returns> + public IEnumerable<TrackingSpanNode<T>> FindNodesIntersecting(SnapshotSpan span) + { + return FindNodesIntersecting(new NormalizedSnapshotSpanCollection(span)); + } + + /// <summary> + /// Find nodes that intersect the given collection of spans (nodes that intersect more than one of the + /// spans are only returned once). + /// </summary> + /// <param name="spans">The collection of spans to search.</param> + /// <returns>Nodes that intersect the given collection of spans.</returns> + public IEnumerable<TrackingSpanNode<T>> FindNodesIntersecting(NormalizedSnapshotSpanCollection spans) + { + return FindNodes(spans, Root, recurse: true, contained: false); + } + + /// <summary> + /// Find nodes that intersect the given span that are at the top level of the tree (have no parent nodes). + /// </summary> + /// <param name="span">The span to search.</param> + /// <returns>Nodes that are toplevel and intersect the given span.</returns> + public IEnumerable<TrackingSpanNode<T>> FindTopLevelNodesIntersecting(SnapshotSpan span) + { + return FindTopLevelNodesIntersecting(new NormalizedSnapshotSpanCollection(span)); + } + + /// <summary> + /// Find nodes that intersect the given collection of spans that are at the top level of the tree (have no parent nodes). + /// </summary> + /// <param name="spans">The collection of spans to search.</param> + /// <returns>Nodes that are toplevel and intersect the given collection of spans.</returns> + public IEnumerable<TrackingSpanNode<T>> FindTopLevelNodesIntersecting(NormalizedSnapshotSpanCollection spans) + { + return FindNodes(spans, Root, recurse: false, contained: false); + } + + /// <summary> + /// Find nodes that are contained completely by the given span. + /// </summary> + /// <param name="span">The span to search.</param> + /// <returns>Nodes that are contained completely by the given span.</returns> + public IEnumerable<TrackingSpanNode<T>> FindNodesContainedBy(SnapshotSpan span) + { + return FindNodesContainedBy(new NormalizedSnapshotSpanCollection(span)); + } + + /// <summary> + /// Find nodes that are contained completely by the given collection of spans. + /// </summary> + /// <param name="spans">The collection of spans to search.</param> + /// <returns>Nodes that are contained completely by the given collection of spans.</returns> + public IEnumerable<TrackingSpanNode<T>> FindNodesContainedBy(NormalizedSnapshotSpanCollection spans) + { + return FindNodes(spans, Root, recurse: true, contained: true); + } + + /// <summary> + /// Find nodes that are contained completely by the given span that are at the top level of the tree (have no parent nodes). + /// </summary> + /// <param name="span">The span to search.</param> + /// <returns>Nodes that are toplevel and are contained completely by the given span.</returns> + public IEnumerable<TrackingSpanNode<T>> FindTopLevelNodesContainedBy(SnapshotSpan span) + { + return FindTopLevelNodesContainedBy(new NormalizedSnapshotSpanCollection(span)); + } + + /// <summary> + /// Find nodes that are contained completely by the given collection of spans that are at the top level of the tree (have no parent nodes). + /// </summary> + /// <param name="spans">The collection of spans to search.</param> + /// <returns>Nodes that are toplevel and are contained completely by the given collection of spans.</returns> + public IEnumerable<TrackingSpanNode<T>> FindTopLevelNodesContainedBy(NormalizedSnapshotSpanCollection spans) + { + return FindNodes(spans, Root, recurse: false, contained: true); + } + + /// <summary> + /// Check if a given point is contained inside of a node (so the point is between the start and end points of a node). + /// </summary> + /// <param name="point">The point to check.</param> + /// <returns><c>true</c> if the poin is inside a node.</returns> + public bool IsPointContainedInANode(SnapshotPoint point) + { + return FindChild(point, Root.Children, left: true).Type == FindResultType.Inner; + } + + /// <summary> + /// Check if a given node is a toplevel node (has no parents). + /// </summary> + /// <param name="node">The node to check.</param> + /// <returns><c>true</c> if the node has no parent node.</returns> + public bool IsNodeTopLevel(TrackingSpanNode<T> node) + { + return Root.Children.Contains(node); + } + + public void Advance(ITextVersion toVersion) + { + if (toVersion == null) + { + throw new ArgumentNullException("toVersion"); + } + + if (toVersion.VersionNumber > this.advanceVersion) + { + this.advanceVersion = toVersion.VersionNumber; + Root.Advance(toVersion); + } + } + + private void OnBufferChanged(object sender, TextContentChangedEventArgs args) + { + // this only runs in stress mode. It introduces synchronous processing on every buffer change + // to advance all the tracking spans in the tree. This prevents these spans from pinning old + // versions and obscuring leaks. + this.Advance(args.After.Version); + } + + #region Static helpers + + static IEnumerable<TrackingSpanNode<T>> FindNodes(NormalizedSnapshotSpanCollection spans, TrackingSpanNode<T> root, bool recurse = true, bool contained = false) + { + if (spans == null || spans.Count == 0 || root.Children.Count == 0) + yield break; + + int requestIndex = 0; + SnapshotSpan currentRequest = spans[requestIndex]; + + // Find the first child + FindResult findResult = FindChild(currentRequest.Start, root.Children, left: true); + int childIndex = findResult.Index; + + if (childIndex >= root.Children.Count) + yield break; + + ITextSnapshot snapshot = currentRequest.Snapshot; + SnapshotSpan currentChild = root.Children[childIndex].TrackingSpan.GetSpan(snapshot); + + while (requestIndex < spans.Count && childIndex < root.Children.Count) + { + if (currentRequest.Start > currentChild.End) + { + // Find the next child + childIndex = FindNextChild(root, currentRequest.Start, childIndex); + + if (childIndex < root.Children.Count) + currentChild = root.Children[childIndex].TrackingSpan.GetSpan(snapshot); + } + else if (currentChild.Start > currentRequest.End) + { + // Skip to the next request + if (++requestIndex < spans.Count) + currentRequest = spans[requestIndex]; + } + else + { + // Yield the region then move to the next + if (!contained || currentRequest.Contains(currentChild)) + yield return root.Children[childIndex]; + + if (recurse) + { + foreach (var result in FindNodes(spans, root.Children[childIndex], recurse, contained)) + yield return result; + } + + // Find the next child + childIndex = FindNextChild(root, currentRequest.Start, childIndex); + + if (childIndex < root.Children.Count) + currentChild = root.Children[childIndex].TrackingSpan.GetSpan(snapshot); + } + } + } + + static int FindNextChild(TrackingSpanNode<T> root, SnapshotPoint point, int currentChildIndex) + { + // If we're already at the end, there's no need to continue searching + if (currentChildIndex == root.Children.Count - 1) + return currentChildIndex + 1; + + return FindChild(point, root.Children, left: true, lo: currentChildIndex + 1).Index; + } + + static bool RemoveItemFromRoot(T item, SnapshotSpan span, TrackingSpanNode<T> root) + { + if (root.Children.Count == 0) + return false; + + var result = FindChild(span.Start, root.Children, left: true); + + if (result.Index < 0 || result.Index >= root.Children.Count) + return false; + + // Search from this index onward (there may be empty regions in the way) + for (int i = result.Index; i < root.Children.Count; i++) + { + var child = root.Children[i]; + SnapshotSpan childSpan = child.TrackingSpan.GetSpan(span.Snapshot); + + // Check to see if we've walked past it + if (childSpan.Start > span.End) + { + return false; + } + else if (childSpan == span && object.Equals(child.Item, item)) + { + root.Children.RemoveAt(i); + root.Children.InsertRange(i, child.Children); + return true; + } + else if (childSpan.Contains(span)) + { + if (RemoveItemFromRoot(item, span, child)) + return true; + } + } + + return false; + } + + static TrackingSpanNode<T> TryAddNodeToRoot(TrackingSpanNode<T> newNode, SnapshotSpan span, TrackingSpanNode<T> root) + { + var children = root.Children; + + if (children.Count == 0) + { + children.Add(newNode); + return newNode; + } + + FindResult leftResult = FindIndexForAdd(span.Start, children, left: true); + FindResult rightResult = FindIndexForAdd(span.End, children, left: false); + + // See if we can add the node anywhere + + // The indices cross if the searches fail, and the node is inside a gap. + if (leftResult.Index > rightResult.Index) + { + // Case #1: If the new node should go in a gap between two nodes, just insert it in the correct location + Debug.Assert(leftResult.Type == FindResultType.Outer || rightResult.Type == FindResultType.Outer); + + children.Insert(leftResult.Index, newNode); + return newNode; + } + else + { + if (leftResult.Type == FindResultType.Inner || rightResult.Type == FindResultType.Inner) + { + // Case #2: The new node is contained entirely in a single child node, so add it to that child + // Check the nodes at either end of the resulting indices (they may not be the same index due to + // 0-length nodes that abut the correct child). + if (children[leftResult.Index].TrackingSpan.GetSpan(span.Snapshot).Contains(span)) + return TryAddNodeToRoot(newNode, span, children[leftResult.Index]); + if (leftResult.Index != rightResult.Index && children[rightResult.Index].TrackingSpan.GetSpan(span.Snapshot).Contains(span)) + return TryAddNodeToRoot(newNode, span, children[rightResult.Index]); + + // This fails if the node isn't fully contained in a single child + } + else + { + // Case #3: The new node contains any number of children, so we: + int start = leftResult.Index; + int count = rightResult.Index - leftResult.Index + 1; + + // a) Add all the children this should contain to the new node, + newNode.Children.AddRange(children.Skip(start).Take(count)); + // b) Remove them from the existing root node, and + children.RemoveRange(start, count); + // c) Add the new node in their place + children.Insert(start, newNode); + + return newNode; + } + } + + // We couldn't find a place to add this node, so return failure + return null; + } + + + // Find the child that intersects the given point (or child gap, if no nodes intersect). + // When left is true, finds the left-most child intersecting the point. Otherwise, finds the right-most child intersecting the point. + static FindResult FindChild(SnapshotPoint point, List<TrackingSpanNode<T>> nodes, bool left, int lo = -1, int hi = -1) + { + if (nodes.Count == 0) + return new FindResult() { Index = 0, Intersects = false, Type = FindResultType.Outer }; + + ITextSnapshot snapshot = point.Snapshot; + int position = point.Position; + + FindResultType type = FindResultType.Outer; + + bool intersects = false; + + // Binary search for the node containing the position + if (lo == -1) + lo = 0; + if (hi == -1) + hi = nodes.Count - 1; + + int mid = lo; + + SnapshotSpan midSpan = new SnapshotSpan(); + while (lo <= hi) + { + mid = (lo + hi) / 2; + + midSpan = nodes[mid].TrackingSpan.GetSpan(snapshot); + + if (position < midSpan.Start) + { + hi = mid - 1; + } + else if (position > midSpan.End) + { + lo = mid + 1; + } + else + { + // midSpan contains or abuts the position + if (position > midSpan.Start && position < midSpan.End) + type = FindResultType.Inner; + + intersects = true; + + break; + } + } + + int index = mid; + midSpan = nodes[index].TrackingSpan.GetSpan(snapshot); + + // If this is an intersection, make sure we walk to the left or right as requested. + if (intersects) + { + if (left) + { + while (index >= lo) + { + midSpan = nodes[index].TrackingSpan.GetSpan(snapshot); + if (position > midSpan.End) + { + index++; + break; + } + + index--; + } + + // If we fell off the end, just return lo + if (index < lo) + index = lo; + } + else + { + while (index <= hi) + { + midSpan = nodes[index].TrackingSpan.GetSpan(snapshot); + if (position < midSpan.Start) + { + index--; + break; + } + + index++; + } + + // If we fell off the end, just return hi + if (index > hi) + index = hi; + } + } + + return new FindResult() { Type = type, Index = index, Intersects = intersects}; + } + + // Find the left/right indices for adding a new node in the scope of the given list of nodes. + // When left is true (and the point is not inside a node), finds the leftmost node with a start and end point >= the position. + // When left is false (and the point is not inside a node), finds the rightmost node with a start and end point <= the position. + static FindResult FindIndexForAdd(SnapshotPoint point, List<TrackingSpanNode<T>> nodes, bool left, int lo = -1, int hi = -1) + { + ITextSnapshot snapshot = point.Snapshot; + int position = point.Position; + + if (lo == -1) + lo = 0; + if (hi == -1) + hi = nodes.Count - 1; + + // Use the general FindIndex to start + FindResult result = FindChild(point, nodes, left, lo, hi); + + int index = result.Index; + SnapshotSpan midSpan = nodes[index].TrackingSpan.GetSpan(snapshot); + + // If we hit a gap, figure out the correct index to use + if (!result.Intersects) + { + // midSpan is the last span we found + if (position < midSpan.Start && !left) + index--; + else if (position > midSpan.End && left) + index++; + } + else if (result.Type == FindResultType.Outer) + { + // For an outer hit, we need to walk left or right to make sure we're containing all empty regions + if (left) + { + while (index <= hi) + { + midSpan = nodes[index].TrackingSpan.GetSpan(snapshot); + if (position <= midSpan.Start) + { + break; + } + + index++; + } + } + else + { + while (index >= lo) + { + midSpan = nodes[index].TrackingSpan.GetSpan(snapshot); + if (position >= midSpan.End) + { + break; + } + + index--; + } + } + } + + return new FindResult() { Type = result.Type, Index = index, Intersects = result.Intersects }; + } + + enum FindResultType + { + Inner, // The result found is inside a node + Outer // The result found is outside of nodes + } + + struct FindResult + { + // Inner or outer, depending on where the result is + public FindResultType Type; + // The child index of the result + public int Index; + // true when the result intersects the given index + public bool Intersects; + } + #endregion + } + + /// <summary> + /// A node in the tracking span tree. A node contains a data item, an associated tracking span, + /// and a (possibly empty) list of children, which can be modified as items are inserted and removed into the tree. + /// </summary> + public sealed class TrackingSpanNode<T> + { + public TrackingSpanNode(T item, ITrackingSpan trackingSpan) : this(item, trackingSpan, new List<TrackingSpanNode<T>>()) { } + + public TrackingSpanNode(T item, ITrackingSpan trackingSpan, List<TrackingSpanNode<T>> children) + { + Item = item; + TrackingSpan = trackingSpan; + Children = children; + } + + public T Item { get; private set; } + public ITrackingSpan TrackingSpan { get; private set; } + public List<TrackingSpanNode<T>> Children { get; private set; } + + internal void Advance(ITextVersion toVersion) + { + if (TrackingSpan != null) + TrackingSpan.GetSpan(toVersion); + + foreach (var child in Children) + { + child.Advance(toVersion); + } + } + } +} diff --git a/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs b/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs new file mode 100644 index 0000000..6ed9e0a --- /dev/null +++ b/src/Text/Def/Internal/TextData/UnicodeWordExtent.cs @@ -0,0 +1,691 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Utilities +{ + using System; + using System.Globalization; + using Microsoft.VisualStudio.Text; + + /// <summary> + /// This is a little buffer over the contents of a line, so that we avoid fetching the entirety of giant lines when we really + /// just need to look at a small local area. + /// </summary> + public class LineBuffer + { + public static int BufferSize = 1024; // this is non-constant so unit tests can change it to better test LineBuffer logic + + private readonly ITextSnapshotLine line; + + /// <summary> + /// Current window into the line. Size is less than or equal to BufferSize. + /// </summary> + private string contents; + + /// <summary> + /// Bounds of the current contents, 0-origin with respect to the line. + /// </summary> + private Span extent; + + public LineBuffer(ITextSnapshotLine line) + { + this.line = line; + if (line.LengthIncludingLineBreak <= BufferSize) + { + this.contents = line.GetTextIncludingLineBreak(); + this.extent = new Span(0, line.LengthIncludingLineBreak); + } + else + { + this.extent = new Span(0, 0); + } + } + + public char this[int index] + { + get + { + if (!this.extent.Contains(index)) + { + // shift the window so that it is centered on "index" + int start = Math.Max(0, index - BufferSize / 2); + int end = Math.Min(line.LengthIncludingLineBreak, start + BufferSize); + this.extent = Span.FromBounds(start, end); + this.contents = line.Snapshot.GetText(line.Start + start, this.extent.Length); + return this.contents[index - this.extent.Start]; + } + return this.contents[index - this.extent.Start]; + } + } + } + + public class UnicodeWordExtent + { + static public bool FindCurrentToken(SnapshotPoint currentPosition, out SnapshotSpan span) + { + ITextSnapshotLine textSnapshotLine = currentPosition.GetContainingLine(); + LineBuffer lineText = new LineBuffer(textSnapshotLine); + int lineLength = textSnapshotLine.LengthIncludingLineBreak; + int iCol = (currentPosition - textSnapshotLine.Start); + + // handle end of buffer case + if (iCol >= lineLength) + { + span = new SnapshotSpan(currentPosition.Snapshot, textSnapshotLine.End, 0); + return true; + } + + // scan left for base char + while ((iCol > 0) && !IsGraphemeBreak(lineText, iCol)) + { + iCol--; + } + + // if it's a word, return the word + char ch = lineText[iCol]; + if (IsWordChar(ch)) + { + return FindCurrentWordCoords(new SnapshotPoint(currentPosition.Snapshot, textSnapshotLine.Start + iCol), out span); + } + + // contiguous whitespace + int iBeg; + if (Char.IsWhiteSpace(ch)) + { + for (iBeg = iCol - 1; iBeg >= 0; --iBeg) + { + if (!Char.IsWhiteSpace(lineText[iBeg])) + break; + } + iBeg++; + + for (++iCol; iCol < lineLength; ++iCol) + { + if (!Char.IsWhiteSpace(lineText[iCol])) + break; + } + } + + // contiguous punctuation and math symbols + else if (Char.IsPunctuation(ch) || IsMathSymbol(ch)) + { + for (iBeg = iCol - 1; iBeg >= 0; --iBeg) + { + ch = lineText[iBeg]; + if (!(Char.IsPunctuation(ch) || IsMathSymbol(ch))) + break; + } + iBeg++; + + for (++iCol; iCol < lineLength; ++iCol) + { + ch = lineText[iCol]; + if (!(Char.IsPunctuation(ch) || IsMathSymbol(ch))) + break; + } + } + + //Let's get the whole surrogate pair. + else if (Char.IsHighSurrogate(ch) && ((iCol + 1) < lineLength) && Char.IsLowSurrogate(lineText[iCol + 1])) + { + iBeg = iCol; + iCol += 2; + } + + // any other single char + else + { + iBeg = iCol++; + } + + if (iCol > lineLength) + { + iCol = lineLength; + } + + // Done -- fill in the data + span = new SnapshotSpan(currentPosition.Snapshot, (textSnapshotLine.Start + iBeg), (iCol - iBeg)); + + return true; + } + + //--------------------------------------------------------------------------- + // FindCurrentWordCoords + //--------------------------------------------------------------------------- + public static bool FindCurrentWordCoords(SnapshotPoint currentPosition, out SnapshotSpan span) + { + span = new SnapshotSpan(currentPosition, 0); + + ITextSnapshotLine textSnapshotLine = currentPosition.GetContainingLine(); + LineBuffer lineText = new LineBuffer(textSnapshotLine); + int lineLength = textSnapshotLine.LengthIncludingLineBreak; + int iCol = (currentPosition - textSnapshotLine.Start); + + // scan left for base char + while ((iCol > 0) && !IsGraphemeBreak(lineText, iCol)) + { + iCol--; + } + + if ((iCol == lineLength) || !IsWordChar(lineText[iCol])) + { + if (iCol == 0 || !IsWordChar(lineText[iCol - 1])) + { + return false; + } + iCol--; + } + + // Find the beginning + int iBeg; + for (iBeg = iCol; iBeg >= 0; --iBeg) + { + if (IsWordBreak(lineText, iBeg, false)) + break; + } + + // Find the end + for (++iCol; iCol < lineLength; ++iCol) + { + if (IsWordBreak(lineText, iCol, false)) + break; + } + + // Done -- fill in the data + span = new SnapshotSpan(currentPosition.Snapshot, (textSnapshotLine.Start + iBeg), (iCol - iBeg)); + + return true; + } + + public static bool IsWordBreak(LineBuffer line, int iChar, bool fHanWordBreak) + { + if (iChar <= 0) + return true; + + if (!IsGraphemeBreak(line, iChar)) + return false; + + char cR = line[iChar]; + + int iCharPrev = PrevChar(line, iChar); + if (iCharPrev < 0) + return true; + char cL = line[iCharPrev]; + + if (IsNonBreaking(cL) || IsNonBreaking(cR)) + return false; + + return IsPropBreak(cL, cR, fHanWordBreak); + } + + public static int PrevChar(LineBuffer line, int iChar) + { + if (iChar <= 0) + return -1; + + for (--iChar; !IsGraphemeBreak(line, iChar); --iChar) ; + + return iChar; + } + + // Returns true if iChar is at a grapheme boundary + public static bool IsGraphemeBreak(LineBuffer line, int iChar) + { + if (iChar <= 0) + return true; + + char cR = line[iChar]; + if (cR == 0) + return true; + + char cL = line[(iChar - 1)]; + if (cL == 0) + return true; + + // break around line breaks + if (IsLineBreak(cL) || IsLineBreak(cR)) + return true; + + // don't separate a combining char from it's base + if (IsCombining(cR)) + { + // A combining character after a quote or whitespace does not combine with the quote or whitespace + if (cL != '\'' && cL != '\"' && !Char.IsWhiteSpace(cL) && Char.IsLetter(cL)) + { + return false; + } + + } + + // don't break surrogate pairs + if (Char.IsHighSurrogate(cL)) + { + return !Char.IsLowSurrogate(cR); + } + + HangulJamoType hjL = GetHangulJamoType(cL); + HangulJamoType hjR = GetHangulJamoType(cR); + if ((hjL != HangulJamoType.Other) && (hjR != HangulJamoType.Other)) + { + switch (hjL) + { + case HangulJamoType.Lead: + return false; + case HangulJamoType.Vowel: + return (HangulJamoType.Lead == hjR); + case HangulJamoType.Trail: + return (HangulJamoType.Trail != hjR); + default: + break; + } + } + return true; + } + + // Modified algorithm from: + // [U2] 5.13 <Locating Text Element Boundaries> 'Word Boundaries' + // + // The rule notation here uses "//" for the italic double dagger used in [U2], + // and ! for the 'not' symbol. + // + // We include decimal digits and LOW LINE ('_') in the definition of a 'word' + // + public static bool IsPropBreak(char cL, char cR, bool fHanWordBreak) + { + UnicodeCategory R = Char.GetUnicodeCategory(cR); + if (IsPropCombining(R)) + return false; + + // Rule (1 usually does not occur (no line/para bounds in the text we're searching) + // or will be caught by (2 and (3. + + // Rule (1 Para // + // Rule (2 !Let // Let + // Rule (3 Let // !Let + bool fL = IsWordChar(cL); + bool fR = IsWordChar(cR); + if (fL != fR) + return true; + + // break between any two non-word-chars + if (!fL && !fR) + return true; + + // Break between characters of different scripts (e.g. Arabic/English) + // unless one is a letter & the other is a digit + UnicodeScript LScript = UScript(cL); + UnicodeScript RScript = UScript(cR); + if (LScript != RScript) + { + UnicodeCategory L = Char.GetUnicodeCategory(cL); + if ((IsPropAlpha(L) && IsPropAlpha(R)) || + (IsPropDigit(L) && IsPropDigit(R))) + return true; + } + + if ((LScript == UnicodeScript.CJK) || (RScript == UnicodeScript.CJK)) + { + bool fLHan = IsIdeograph(cL); + bool fRHan = IsIdeograph(cR); + if (fHanWordBreak && (fLHan || fRHan)) + return true; + + // Rule (4 !(Hira|Kata|Han) // Hira|Kata|Han + if ((LScript != UnicodeScript.CJK) && (RScript == UnicodeScript.CJK)) + return true; + + // Rule (5 Hira // !Hira + bool fRHiragana = IsHiragana(cR); + if (IsHiragana(cL) && !fRHiragana) + return true; + + // Rule (6 Kata // !(Hira|Kata) + if (IsKatakana(cL) && !(fRHiragana || IsKatakana(cR))) + return true; + + // Rule (7 Han // !(Hira|Han) + if (fLHan && !(fRHan || fRHiragana)) + return true; + } + + return false; + } + + public static bool IsWordChar(char ch) + { + return Char.IsLetterOrDigit(ch) || (ch == '_') || (ch == '$'); + } + + /// <summary> + /// Logic from vscommon\unilib\uniword.cpp + /// </summary> + public static bool IsWholeWord(SnapshotSpan candidate, bool acceptHanWordBreak = true) + { + ITextSnapshotLine line = candidate.Start.GetContainingLine(); + LineBuffer lineBuffer = null; + + // A word that spans over two lines is not a whole word + if (line.Extent.End < candidate.End) + { + return false; + } + + bool isLeftBreak = candidate.Start == 0; + bool isRightBreak = candidate.End == candidate.Snapshot.Length; + + if (!isLeftBreak || !isRightBreak) + { + lineBuffer = new LineBuffer(line); + } + + if (!isLeftBreak) + { + isLeftBreak = IsWordBreak(lineBuffer, candidate.Start - line.Start, acceptHanWordBreak); + + if (!isLeftBreak) + { + return false; + } + } + + if (!isRightBreak) + { + isRightBreak = IsWordBreak(lineBuffer, candidate.End - line.Start, acceptHanWordBreak); + } + + return isLeftBreak && isRightBreak; + } + + public static bool IsNonBreaking(char ch) + { + return ((ch == 0x00a0) || // NO-BREAK SPACE + (ch == 0x2011) || // NON-BREAKING HYPHEN + (ch == 0x202F) || // NARROW NO-BREAK SPACE + (ch == 0x30FC) || // Japanese NO-BREAK character a bit like a long hyphen + (ch == 0xfeff)); // ZERO WIDTH NO-BREAK SPACE (byte order mark) + } + + public const char UCH_LF = (char)0x000A; + public const char UCH_CR = (char)0x000D; + public const char UCH_LS = (char)0x2028; + public const char UCH_PS = (char)0x2029; + public const char UCH_NEL = (char)0x0085; + public static bool IsLineBreak(char ch) + { + return (ch <= UCH_PS) && (UCH_CR == ch || UCH_LF == ch || UCH_LS == ch || UCH_PS == ch || UCH_NEL == ch); + } + + public enum UnicodeScript + { + // Pre-Unicode 3.0 scripts + + NONE = 0x0000, // punctuation, dingbat, symbol, math, ... + LATIN = 0x0001, + GREEK = 0x0002, + CYRILLIC = 0x0003, + ARMENIAN = 0x0004, + HEBREW = 0x0005, + ARABIC = 0x0006, + DEVANAGARI = 0x0007, + BANGLA = 0x0008, + GURMUKHI = 0x0009, + GUJARATI = 0x000a, + ODIA = 0x000b, + TAMIL = 0x000c, + TELUGU = 0x000d, + KANNADA = 0x000e, + MALAYALAM = 0x000f, + THAI = 0x0010, + LAO = 0x0011, + TIBETAN = 0x0012, + GEORGIAN = 0x0013, + CJK = 0x0014, // Chinese, Japanese, Korean (Han, Katakana, Hiragana, Hangul) + + // Unicode 3.0 added scripts + + BRAILLE = 0x0015, + SYRIAC = 0x0016, + THAANA = 0x0017, + SINHALA = 0x0018, + MYANMAR = 0x0019, + ETHIOPIC = 0x001a, + CHEROKEE = 0x001b, + CANADIAN_ABORIGINAL = 0x001c, + OGHAM = 0x001d, + RUNIC = 0x001e, + KHMER = 0x001f, + MONGOLIAN = 0x0020, + YI = 0x0021 + } + + public static UnicodeScript UScript(char ch) + { + if (ch <= 0x024F) return UnicodeScript.LATIN; // 0x0000, 0x007F, Basic Latin + // 0x0080, 0x00FF, Latin-1 Supplement + // 0x0100, 0x017F, Latin Extended-A + // 0x0180, 0x024F, Latin Extended-B + if (ch < 0x2000) + { + if (ch < 0x1000) + { + if (ch < 0x0370) return UnicodeScript.NONE; // 0x0250, 0x02AF, IPA Extensions + // 0x02B0, 0x02FF, Spacing Modifier Letters + // 0x0300, 0x036F, Combining Diacritical Marks + if (ch < 0x0400) return UnicodeScript.GREEK; // 0x0370, 0x03FF, Greek + if (ch <= 0x04FF) return UnicodeScript.CYRILLIC; // 0x0400, 0x04FF, Cyrillic + if (ch < 0x0530) return UnicodeScript.NONE; // 0x0500, 0x052F, NONE + if (ch < 0x0590) return UnicodeScript.ARMENIAN; // 0x0530, 0x058F, Armenian + if (ch < 0x0600) return UnicodeScript.HEBREW; // 0x0590, 0x05FF, Hebrew + if (ch < 0x0700) return UnicodeScript.ARABIC; // 0x0600, 0x06FF, ARABIC + if (ch <= 0x074F) return UnicodeScript.SYRIAC; // 0x0700, 0x074F, SYRIAC + if (ch < 0x0780) return UnicodeScript.NONE; // 0x0750, 0x077F, NONE + if (ch <= 0x07BF) return UnicodeScript.THAANA; // 0x0780, 0x07BF, THAANA + if (ch < 0x0900) return UnicodeScript.NONE; // 0x07C0, 0x08FF, NONE + if (ch < 0x0980) return UnicodeScript.DEVANAGARI; // 0x0900, 0x097F, DEVANAGARI + if (ch < 0x0A00) return UnicodeScript.BANGLA; // 0x0980, 0x09FF, BANGLA + if (ch < 0x0A80) return UnicodeScript.GURMUKHI; // 0x0A00, 0x0A7F, GURMUKHI + if (ch < 0x0B00) return UnicodeScript.GUJARATI; // 0x0A80, 0x0AFF, GUJARATI + if (ch < 0x0B80) return UnicodeScript.ODIA; // 0x0B00, 0x0B7F, ODIA + if (ch < 0x0C00) return UnicodeScript.TAMIL; // 0x0B80, 0x0BFF, TAMIL + if (ch < 0x0C80) return UnicodeScript.TELUGU; // 0x0C00, 0x0C7F, TELUGU + if (ch < 0x0D00) return UnicodeScript.KANNADA; // 0x0C80, 0x0CFF, KANNADA + if (ch < 0x0D80) return UnicodeScript.MALAYALAM; // 0x0D00, 0x0D7F, MALAYALAM + if (ch < 0x0E00) return UnicodeScript.SINHALA; // 0x0D80, 0x0DFF, SINHALA + if (ch < 0x0E80) return UnicodeScript.THAI; // 0x0E00, 0x0E7F, THAI + if (ch < 0x0F00) return UnicodeScript.LAO; // 0x0E80, 0x0EFF, LAO + + return UnicodeScript.TIBETAN; // 0x0F00, 0x0FFF, TIBETAN + } + else + { + if (ch < 0x10A0) return UnicodeScript.MYANMAR; // 0x1000, 0x109F, Myanmar + if (ch < 0x1100) return UnicodeScript.GEORGIAN; // 0x10A0, 0x10FF, Georgian + if (ch < 0x1200) return UnicodeScript.CJK; // 0x1100, 0x11FF, Hangul Jamo + if (ch < 0x13A0) return UnicodeScript.ETHIOPIC; // 0x1200, 0x139F, Ethiopic + if (ch < 0x1400) return UnicodeScript.CHEROKEE; // 0x13A0, 0x13FF, Cherokee + if (ch < 0x1680) return UnicodeScript.CANADIAN_ABORIGINAL; // 0x1400, 0x167F, Unified Canadian Aboriginal Syllabics + if (ch < 0x16A0) return UnicodeScript.OGHAM; // 0x1680, 0x169F, Ogham + if (ch < 0x1780) return UnicodeScript.RUNIC; // 0x16A0, 0x177F, Runic + if (ch < 0x1800) return UnicodeScript.KHMER; // 0x1780, 0x17FF, Khmer + if (ch <= 0x18AF) return UnicodeScript.MONGOLIAN; // 0x1800, 0x18AF, Mongolian + if (ch < 0x1E00) return UnicodeScript.NONE; // 0x18B0, 0x1DFF, NONE + if (ch < 0x1F00) return UnicodeScript.LATIN; // 0x1E00, 0x1EFF, Latin Extended Additional + + return UnicodeScript.GREEK; // 0x1F00, 0x1FFF, Greek Extended + } + } + if (ch < 0xD800) + { + if (ch <= 0x27FF) return UnicodeScript.NONE; // 0x2000, 0x206F, General Punctuation + // 0x2070, 0x209F, Superscripts and Subscripts + // 0x20A0, 0x20CF, Currency Symbols + // 0x20D0, 0x20FF, Combining Marks for Symbols + // 0x2100, 0x214F, Letterlike Symbols + // 0x2150, 0x218F, Number Forms + // 0x2190, 0x21FF, Arrows + // 0x2200, 0x22FF, Mathematical Operators + // 0x2300, 0x23FF, Miscellaneous Technical + // 0x2400, 0x243F, Control Pictures + // 0x2440, 0x245F, Optical Character Recognition + // 0x2460, 0x24FF, Enclosed Alphanumerics + // 0x2500, 0x257F, Box Drawing + // 0x2580, 0x259F, Block Elements + // 0x25A0, 0x25FF, Geometric Shapes + // 0x2600, 0x26FF, Miscellaneous Symbols + // 0x2700, 0x27BF, Dingbats + if (ch <= 0x28FF) return UnicodeScript.BRAILLE; // 0x2800, 0x28FF, Braille Patterns + if (ch < 0x2E80) return UnicodeScript.NONE; // 0x2900, 0x2E7F, NONE + if (ch <= 0x31BF) return UnicodeScript.CJK; // 0x2E80, 0x2EFF, CJK Radicals Supplement + // 0x2F00, 0x2FDF, Kangxi Radicals + // 0x2FF0, 0x2FFF, Ideographic Description Characters + // 0x3000, 0x303F, CJK Symbols and Punctuation + // 0x3040, 0x309F, Hiragana + // 0x30A0, 0x30FF, Katakana + // 0x3100, 0x312F, Bopomofo + // 0x3130, 0x318F, Hangul Compatibility Jamo + // 0x3190, 0x319F, Kanbun + // 0x31A0, 0x31BF, Bopomofo Extended + if (ch < 0x3200) return UnicodeScript.NONE; // 0x31C0, 0x31FF, NONE + if (ch <= 0x4DBf) return UnicodeScript.CJK; // 0x3200, 0x32FF, Enclosed CJK Letters and Months + // 0x3300, 0x33FF, CJK Compatibility + // 0x3400, 0x4DB5, CJK Unified Ideographs Extension A + if (ch < 0x4E00) return UnicodeScript.NONE; // 0x4DC0, 0x3DFF, NONE + if (ch <= 0x9FFF) return UnicodeScript.CJK; // 0x4E00, 0x9FFF, CJK Unified Ideographs + if (ch <= 0xA4CF) return UnicodeScript.YI; // 0xA000, 0xA48F, Yi Syllables + // 0xA490, 0xA4CF, Yi Radicals + if (ch < 0xAC00) return UnicodeScript.NONE; // 0xA4D0, 0xABFF, NONE + if (ch <= 0xD7A3) return UnicodeScript.CJK; // 0xAC00, 0xD7A3, Hangul Syllables + + return UnicodeScript.NONE; // 0xD7A4, 0xD7FF, NONE + } + if (ch < 0xF900) return UnicodeScript.NONE; // 0xD800, 0xDB7F, High Surrogates + // 0xDB80, 0xDBFF, High Private Use Surrogates + // 0xDC00, 0xDFFF, Low Surrogates + // 0xE000, 0xF8FF, Private Use + if (ch < 0xFB00) return UnicodeScript.CJK; // 0xF900, 0xFAFF, CJK Compatibility Ideographs + if (ch < 0xFB4F) return UnicodeScript.LATIN; // 0xFB00, 0xFB4F, Alphabetic Presentation Forms + if (ch < 0xFE00) return UnicodeScript.ARABIC; // 0xFB50, 0xFDFF, Arabic Presentation Forms-A + if (ch < 0xFE30) return UnicodeScript.NONE; // 0xFE20, 0xFE2F, Combining Half Marks + if (ch < 0xFE50) return UnicodeScript.CJK; // 0xFE30, 0xFE4F, CJK Compatibility Forms + if (ch < 0xFE70) return UnicodeScript.NONE; // 0xFE50, 0xFE6F, Small Form Variants + if (ch < 0xFEFF) return UnicodeScript.ARABIC; // 0xFE70, 0xFEFE, Arabic Presentation Forms-B + if (ch < 0xFFF0) + { + if (ch == 0xFEFF) return UnicodeScript.NONE; // 0xFEFF, 0xFEFF, Specials + + // 0xFF00, 0xFF00, Halfwidth and Fullwidth Forms (FALLTHROUGH) + + if ((ch >= 0xFF01) && (ch <= 0xFF5E)) + return UnicodeScript.LATIN; // 0xFF01, 0xFF5E, LATIN + + return UnicodeScript.CJK; // 0xFF5E, 0xFFEF, Halfwidth and Fullwidth Forms + } + + return UnicodeScript.NONE; // 0xFFF0, 0xFFFD, Specials + // 0xFFFeE 0xFFFF, NONE + } + + //public static bool IsHangul(char ch) + //{ + // bool f = false; + // if (ch < 0x1100) {} + // else if (ch <= 0x11FF) f = true; // (0x1100 - 0x11FF) Hangul Combining Jamo + // else if (ch < 0x3130) {} + // else if (ch <= 0x318f) f = true; // (0x3130 - 0x318f) Hangul Compatability Jamo + // else if (ch < 0xac00) {} + // else if (ch <= 0xd7a3) f = true; // (0xac00 - 0xd7a3) Hangul Syllables + // else if (ch < 0xffa0) {} + // else if (ch <= 0xffdf) f = true; // (0xffa0 - 0xffdf) Halfwidth Hangul Compatability Jamo + // return f; + //} + + public static bool IsKatakana(char ch) + { + // Odd short circuit logic but directly ported from unilib + return ((ch >= 0x3031) && + (((ch >= 0x3099) && (ch <= 0x30fe)) || + ((ch >= 0xff66) && (ch <= 0xff9f)))); + } + + public static bool IsHiragana(char ch) + { + // Odd short circuit logic but directly ported from unilib + return ((ch >= 0x3031) && + (((ch >= 0x3041) && (ch <= 0x309e)) || + (0x30fc == ch) || (0xff70 == ch))); // fullwidth and halfwidth hira/kata prolonged sound mark + } + + public static bool IsIdeograph(char ch) + { + bool f = false; + if (ch < 0x4e00) { if (ch == 0x3005) f = true; } + else if (ch <= 0x9fa5) f = true; + else if (ch < 0xf900) {} + else if (ch <= 0xfa2d) f = true; + return f; + } + + public static bool IsMathSymbol(char ch) + { + return (Char.GetUnicodeCategory(ch) == UnicodeCategory.MathSymbol); + } + + public static bool IsCombining(char ch) + { + return IsPropCombining(Char.GetUnicodeCategory(ch)); + } + + public static bool IsPropCombining(UnicodeCategory cat) + { + return ((cat >= UnicodeCategory.NonSpacingMark) && (cat <= UnicodeCategory.EnclosingMark)); + } + + public static bool IsPropAlpha(UnicodeCategory cat) + { + return (cat <= UnicodeCategory.OtherLetter); + } + public static bool IsPropDigit(UnicodeCategory cat) + { + return (cat == UnicodeCategory.DecimalDigitNumber); + } + + public const char UCH_HANGUL_JAMO_FIRST = (char)0x1100; + public const char UCH_HANGUL_JAMO_LEAD_FIRST = (char)0x1100; + public const char UCH_HANGUL_JAMO_LEAD_LAST = (char)0x115F; + public const char UCH_HANGUL_JAMO_VOWEL_FIRST = (char)0x1160; + public const char UCH_HANGUL_JAMO_VOWEL_LAST = (char)0x11A2; + public const char UCH_HANGUL_JAMO_TRAIL_FIRST = (char)0x11A8; + public const char UCH_HANGUL_JAMO_TRAIL_LAST = (char)0x11F9; + public const char UCH_HANGUL_JAMO_LAST = (char)0x11FF; + //public static bool IsHangulJamo(char ch) + //{ + // return ((ch >= UCH_HANGUL_JAMO_FIRST) && (ch <= UCH_HANGUL_JAMO_LAST)); + //} + + public enum HangulJamoType + { + Other = 0, + Lead = 1, + Vowel = 2, + Trail = 3 + } + + public static HangulJamoType GetHangulJamoType(char ch) + { + if (ch < UCH_HANGUL_JAMO_FIRST) + return HangulJamoType.Other; + if (ch > UCH_HANGUL_JAMO_LAST) + return HangulJamoType.Other; + if (ch <= UCH_HANGUL_JAMO_LEAD_LAST) + return HangulJamoType.Lead; + if (ch <= UCH_HANGUL_JAMO_VOWEL_LAST) + return HangulJamoType.Vowel; + return HangulJamoType.Trail; + } + + } +} diff --git a/src/Text/Def/Internal/TextLogic/ExpandContractSelectionOptions.cs b/src/Text/Def/Internal/TextLogic/ExpandContractSelectionOptions.cs new file mode 100644 index 0000000..b8558d0 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/ExpandContractSelectionOptions.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations +{ + using Microsoft.VisualStudio.Text.Editor; + + /// <summary> + /// Options applicable to Expand and Contract Selection. + /// </summary> + public static class ExpandContractSelectionOptions + { + /// <summary> + /// The option that determines whether or not expand and contract selection is enable for a particular language. + /// </summary> + public const string ExpandContractSelectionEnabledOptionId = "ExpandContractSelectionEnabled"; + public static readonly EditorOptionKey<bool> ExpandContractSelectionEnabledKey = new EditorOptionKey<bool>(ExpandContractSelectionEnabledOptionId); + } +} diff --git a/src/Text/Def/Internal/TextLogic/IAccurateClassifier.cs b/src/Text/Def/Internal/TextLogic/IAccurateClassifier.cs new file mode 100644 index 0000000..4818da7 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/IAccurateClassifier.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Classification +{ + using System.Collections.Generic; + using System.Threading; + + /// <summary> + /// Assigns <see cref="IClassificationType"/> objects to the text in a <see cref="ITextBuffer"/>. + /// </summary> + public interface IAccurateClassifier : IClassifier + { + /// <summary> + /// Gets all the <see cref="ClassificationSpan"/> objects that intersect the given range of text. + /// </summary> + /// <param name="span"> + /// The snapshot span. + /// </param> + /// <returns> + /// A list of <see cref="ClassificationSpan"/> objects that intersect with the given range. + /// </returns> + /// <remarks> + /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected + /// to return final results (however long it takes to compute) instead of quick but tentative results.</para> + /// <para>If the underlying tagger does not support <see cref="IClassifier"/>, then <see cref="IClassifier"/>.GetClassificationSpans(...) is used instead.</para> + /// </remarks> + IList<ClassificationSpan> GetAllClassificationSpans(SnapshotSpan span, CancellationToken cancel); + } +} diff --git a/src/Text/Def/Internal/TextLogic/IAccurateTagAggregator.cs b/src/Text/Def/Internal/TextLogic/IAccurateTagAggregator.cs new file mode 100644 index 0000000..d920b0a --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/IAccurateTagAggregator.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Threading; +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Text.Tagging +{ + /// <summary> + /// Aggregates all the tag providers in a buffer graph for the specified type of tag. + /// </summary> + /// <typeparam name="T">The type of tag returned by the aggregator.</typeparam> + /// <remarks> + /// The default tag aggregator implementation also does the following: + /// for each <see cref="ITagger<T>"/> over which it aggregates tags, if the tagger is + /// <see cref="IDisposable"/>, call Dispose() on it when the aggregator is disposed + /// or when the taggers are dropped. For example, you should call Dispose() when + /// the content type of a text buffer changes or when a buffer is removed from the buffer graph. + /// </remarks> + public interface IAccurateTagAggregator<out T> : ITagAggregator<T> where T : ITag + { + /// <summary> + /// Gets all the tags that intersect the specified <paramref name="span"/> of the same type as the aggregator. + /// </summary> + /// <param name="span">The span to search.</param> + /// <returns>All the tags that intersect the region.</returns> + /// <remarks> + /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected + /// to return final results (however long it takes to compute) instead of quick but tentative results.</para> + /// <para>The default tag aggregator lazily enumerates the tags of its <see cref="ITagger<T>"/> objects. + /// Because of this, the ordering of the returned mapping spans cannot be predicted. + /// If you need an ordered set of spans, you should collect the returned tag spans, after being mapped + /// to the buffer of interest, into a sortable collection.</para> + /// <para>If the underlying tagger does not support <see cref="IAccurateTagger<T>"/>, then <see cref="ITagger<T>"/>.GetTags(...) is used instead.</para> + /// </remarks> + IEnumerable<IMappingTagSpan<T>> GetAllTags(SnapshotSpan span, CancellationToken cancel); + + /// <summary> + /// Gets all the tags that intersect the specified <paramref name="span"/> of the type of the aggregator. + /// </summary> + /// <param name="span">The span to search.</param> + /// <returns>All the tags that intersect the region.</returns> + /// <remarks> + /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected + /// to return final results (however long it takes to compute) instead of quick but tentative results.</para> + /// <para>The default tag aggregator lazily enumerates the tags of its <see cref="ITagger<T>"/> objects. + /// Because of this, the ordering of the returned mapping spans cannot be predicted. + /// If you need an ordered set of spans, you should collect the returned tag spans, after being mapped + /// to the buffer of interest, into a sortable collection.</para> + /// <para>If the underlying tagger does not support <see cref="IAccurateTagger<T>"/>, then <see cref="ITagger<T>"/>.GetTags(...) is used instead.</para> + /// </remarks> + IEnumerable<IMappingTagSpan<T>> GetAllTags(IMappingSpan span, CancellationToken cancel); + + /// <summary> + /// Gets all the tags that intersect the specified <paramref name="snapshotSpans"/> of the type of the aggregator. + /// </summary> + /// <param name="snapshotSpans">The spans to search.</param> + /// <returns>All the tags that intersect the region.</returns> + /// <remarks> + /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected + /// to return final results (however long it takes to compute) instead of quick but tentative results.</para> + /// <para>The default tag aggregator lazily enumerates the tags of its <see cref="ITagger<T>"/> objects. + /// Because of this, the ordering of the returned mapping spans cannot be predicted. + /// If you need an ordered set of spans, you should collect the returned tag spans, after being mapped + /// to the buffer of interest, into a sortable collection.</para> + /// <para>If the underlying tagger does not support <see cref="IAccurateTagger<T>"/>, then <see cref="ITagger<T>"/>.GetTags(...) is used instead.</para> + /// </remarks> + IEnumerable<IMappingTagSpan<T>> GetAllTags(NormalizedSnapshotSpanCollection snapshotSpans, CancellationToken cancel); + + } +} diff --git a/src/Text/Def/Internal/TextLogic/IAccurateTagger.cs b/src/Text/Def/Internal/TextLogic/IAccurateTagger.cs new file mode 100644 index 0000000..5a30b50 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/IAccurateTagger.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System.Threading; +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Text.Tagging +{ + /// <summary> + /// A provider of tags over a buffer. + /// </summary> + /// <typeparam name="T">The type of tags to generate.</typeparam> + public interface IAccurateTagger<out T> : ITagger<T> where T : ITag + { + /// <summary> + /// Gets all the tags that intersect the <paramref name="spans"/>. + /// </summary> + /// <param name="spans">The spans to visit.</param> + /// <returns>A <see cref="ITagSpan{T}"/> for each tag.</returns> + /// <remarks> + /// <para>This method is used when final results are needed (when, for example, when doing color printing) and is expected + /// to return final results (however long it takes to compute) instead of quick but tentative results.</para> + /// <para>Taggers are not required to return their tags in any specific order.</para> + /// <para>The recommended way to implement this method is by using generators ("yield return"), + /// which allows lazy evaluation of the entire tagging stack.</para> + /// </remarks> + IEnumerable<ITagSpan<T>> GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancel); + } +} diff --git a/src/Text/Def/Internal/TextLogic/IElisionTag.cs b/src/Text/Def/Internal/TextLogic/IElisionTag.cs new file mode 100644 index 0000000..eaa4502 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/IElisionTag.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Tagging +{ + /// <summary> + /// Tag indicating spans of text to be excluded from a view. + /// </summary> + /// <remarks> + /// <para> + /// IViewTaggerProviders are querried by the editor implementation with this tag type for views having + /// the <see cref="F:Microsoft.VisualStudio.Text.Editor.PredefinedTextViewRoles.Structured"/> view role. + /// </para> + /// <para> + /// These tags cause text to be hidden but do not result in any outlining UI. + /// IOutliningRegionTags are used to provide data to the outlining manager. + /// </para> + /// </remarks> + public interface IElisionTag : ITag + { + } +} diff --git a/src/Text/Def/Internal/TextLogic/IExperimentationServiceInternal.cs b/src/Text/Def/Internal/TextLogic/IExperimentationServiceInternal.cs new file mode 100644 index 0000000..f3795c8 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/IExperimentationServiceInternal.cs @@ -0,0 +1,20 @@ +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Service for querying the status of A/B experiments. + /// </summary> + public interface IExperimentationServiceInternal + { + /// <summary> + /// Checks whether or not the flight is enabled for this user. + /// </summary> + /// <param name="flightName">A name of a flight, up to 16 characters long.</param> + /// <returns>True if this user has the specific flight enabled.</returns> + /// <remarks> + /// This method uses cached flighting results, meaning, that this method does not + /// block to download flight membership data, but rather, returns false if the data + /// is not yet available. + /// </remarks> + bool IsCachedFlightEnabled(string flightName); + } +} diff --git a/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs new file mode 100644 index 0000000..e78084b --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/ILoggingServiceInternal.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// + +using System.Collections.Generic; +using System; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Allows code in src/Platform to log events. + /// </summary> + /// <remarks> + /// For example, the VS Provider of this inserts data points into the telemetry data stream. + /// </remarks> + public interface ILoggingServiceInternal + { + /// <summary> + /// Post the event named <paramref name="key"/> to the telemetry stream. Additional properties can be appended as name/value pairs in <paramref name="namesAndProperties"/>. + /// </summary> + void PostEvent(string key, params object[] namesAndProperties); + + /// <summary> + /// Post the event named <paramref name="key"/> to the telemetry stream. Additional properties can be appended as name/value pairs in <paramref name="namesAndProperties"/>. + /// </summary> + void PostEvent(string key, IReadOnlyList<object> namesAndProperties); + + void PostEvent( + TelemetryEventType eventType, + string eventName, + TelemetryResult result = TelemetryResult.Success, + params (string name, object property)[] namesAndProperties); + + void PostEvent( + TelemetryEventType eventType, + string eventName, + TelemetryResult result, + IReadOnlyList<(string name, object property)> namesAndProperties); + + /// <summary> + /// Creates and posts a FaultEvent. + /// </summary> + /// <param name="eventName"> + /// An event name following data model schema. + /// It requires that event name is a unique, not null or empty string. + /// It consists of 3 parts and must follows pattern [product]/[featureName]/[entityName]. FeatureName could be a one-level feature or feature hierarchy delimited by "/". + /// For examples, + /// vs/platform/opensolution; + /// vs/platform/editor/lightbulb/fixerror; + /// </param> + /// <param name="description">Fault description</param> + /// <param name="exceptionObject">Exception instance</param> + /// <param name="additionalErrorInfo">Additional information to be added to Watson's ErrorInformation.txt file.</param> + /// <param name="isIncludedInWatsonSample"> + /// Gets or sets a value indicating whether we sample this event locally. Affects Watson only. + /// If false, will not send to Watson: only sends the telemetry event to AI and doesn't call callback. + /// Changing this will force the event to send to Watson. Be careful because it can have big perf impact. + /// If unchanged, it will be set according to the default sample rate. + /// </param> + void PostFault( + string eventName, + string description, + Exception exceptionObject, + string additionalErrorInfo, + bool? isIncludedInWatsonSample); + + /// <summary> + /// Adjust the counter associated with <paramref name="key"/> and <paramref name="name"/> by <paramref name="delta"/>. + /// </summary> + /// <remarks> + /// <para>Counters start at 0.</para> + /// <para>No information is sent over the wire until the <see cref="PostCounters"/> is called.</para> + /// </remarks> + void AdjustCounter(string key, string name, int delta = 1); + + /// <summary> + /// Post all of the counters. + /// </summary> + /// <remarks> + /// <para>The counters are logged as if PostEvent had been called for each key with a list counter names and values.</para> + /// <para>The counters are cleared as a side-effect of this call.</para> + /// </remarks> + void PostCounters(); + } +} diff --git a/src/Text/Def/Internal/TextLogic/IPerformanceMarkerBlockProvider.cs b/src/Text/Def/Internal/TextLogic/IPerformanceMarkerBlockProvider.cs new file mode 100644 index 0000000..d13feb5 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/IPerformanceMarkerBlockProvider.cs @@ -0,0 +1,23 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Allows marking actions for performance logging. + /// </summary> + /// <remarks> + /// For example, the VS editor adapters return MeasurementBlock instances + /// that log ETW events. + /// </remarks> + public interface IPerformanceMarkerBlockProvider + { + IDisposable CreateBlock(string blockName); + } +}
\ No newline at end of file diff --git a/src/Text/Def/Internal/TextLogic/ITextModelOptionsSetter.cs b/src/Text/Def/Internal/TextLogic/ITextModelOptionsSetter.cs new file mode 100644 index 0000000..2f1a48a --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/ITextModelOptionsSetter.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// A service that propagates <see cref="IEditorOptions"/> to the text model component. + /// This is never intended to be part of the public API -- we already have the + /// editor options facilities for that. This is inteded to allow hosting code (e.g. the + /// Visual Studio editor package) to propagate options down to the text model, + /// where EditorOptions isn't visible. + /// </summary> + /// <remarks>This is a MEF component part, and should be imported as follows: + /// [Import] + /// ITextModelOptionsSetter setter = null; + /// </remarks> + public interface ITextModelOptionsSetter + { + /// <summary> + /// Extract options useful to the text model layer and expose them in + /// that layer. + /// </summary> + void SetTextModelOptions(IEditorOptions options); + } +}
\ No newline at end of file diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs b/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs new file mode 100644 index 0000000..57676ee --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/ITextSearchNavigator2.cs @@ -0,0 +1,24 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations +{ + /// <summary> + /// Provides a service to navigate between search results on a <see cref="ITextBuffer"/> and to + /// perform replacements. + /// </summary> + public interface ITextSearchNavigator2 : ITextSearchNavigator + { + /// <summary> + /// Indicates the ranges that should be searched (if any). + /// </summary> + /// <remarks> + /// If this value to a non-null value will effectively override the ITextSearchNavigator.SearchSpan property. + /// </remarks> + NormalizedSnapshotSpanCollection SearchSpans { get; set; } + } +} diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchResultTag.cs b/src/Text/Def/Internal/TextLogic/ITextSearchResultTag.cs new file mode 100644 index 0000000..0990135 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/ITextSearchResultTag.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations +{ + using Microsoft.VisualStudio.Text.Tagging; + + /// <summary> + /// Represents search results that are provided by a search tagger. + /// </summary> + /// <remarks> + /// The <see cref="ITextSearchResultTag"/> is present such that all consumers of search matches have a common way of obtaining the matches. + /// </remarks> + public interface ITextSearchResultTag : ITag + { + } +} diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs b/src/Text/Def/Internal/TextLogic/ITextSearchTagger.cs new file mode 100644 index 0000000..f351d11 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/ITextSearchTagger.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. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations +{ + using System; + + using Microsoft.VisualStudio.Text.Tagging; + + /// <summary> + /// A tagger that tags contents of a buffer based on the search terms that are passed to the object. To + /// obtain an implementation of this interface, import the <see cref="ITextSearchTaggerFactoryService"/> + /// via the Managed Extensibility Framework. + /// </summary> + /// <remarks> + /// <para> + /// All search operations are performed on a low priority background thread and on demand. + /// </para> + /// <para> + /// In order for this tagger to be consumed by the editor, a corresponding <see cref="ITaggerProvider"/> + /// that provides an instance of this tagger must be exported through the Managed Extensibility Framework. + /// </para> + /// </remarks> + /// <example> + /// <code> + /// [Export] + /// [TagType(typeof(T))] + /// [ContentType("any")] + /// class TaggerProvider : ITaggerProvider + /// { + /// [Import] + /// ITextSearchTaggerFactoryService searchTaggerFactory; + /// + /// #region ITaggerProvider Members + /// + /// public ITagger<T> CreateTagger<T>(Microsoft.VisualStudio.Text.ITextBuffer buffer) where T : ITag + /// { + /// ITextSearchTagger<T> tagger = searchTaggerFactory.CreateTextSearchTagger<T>(buffer); + /// + /// tagger.TagTerm(...); + /// + /// return tagger as ITagger<T>; + /// } + /// + /// #endregion + /// } + /// </code> + /// </example> + /// <typeparam name="T"> + /// A derivative of <see cref="ITag"/>. + /// </typeparam> + /// <remarks> + /// The <see cref="ITextSearchTagger{T}"/> expects to be queried for monotonically increasing snapshot versions. If a query + /// is made in the reverse order, the results returned by the tagger for older versions might differ from the results + /// obtained originally for those versions. + /// </remarks> + public interface ITextSearchTagger<T> : ITagger<T> where T : ITag + { + /// <summary> + /// Limits the scope of the tagger to the provided <see cref="NormalizedSnapshotSpanCollection"/>. + /// </summary> + /// <remarks> + /// If the value is set to <c>null</c> the entire range of the buffer will be searched. + /// </remarks> + NormalizedSnapshotSpanCollection SearchSpans { get; set; } + + /// <summary> + /// Starts tagging occurences of the <paramref name="searchTerm"/>. + /// </summary> + /// <param name="searchTerm"> + /// The term to search for. + /// </param> + /// <param name="searchOptions"> + /// The options to use for the search. + /// </param> + /// <param name="tagFactory"> + /// A factory delegate used to generate tags for matches. The delegate is passed as input + /// a <see cref="SnapshotSpan"/> corresponding to a match and is expected to return the corresponding tag. + /// </param> + /// <exception cref="ArgumentException">If <paramref name="searchOptions"/> requests the search to be + /// performed in the reverse direction (see remarks).</exception> + /// <exception cref="ArgumentException">If <paramref name="searchOptions"/> requests the search to be performed with + /// wrap (see remarks).</exception> + /// <remarks> + /// In order to guarantee that the tagger finds all matches in a given span of text, the searches are always + /// performed in the forwards direction with no wrap. If the <paramref name="searchOptions"/> passed to the + /// tagger indicate otherwise, an exception will be thrown. + /// </remarks> + void TagTerm(string searchTerm, FindOptions searchOptions, Func<SnapshotSpan, T> tagFactory); + + /// <summary> + /// Clears any existing tags and all search terms that are being search for. Cancels any + /// ongoing background searches. + /// </summary> + void ClearTags(); + } +} diff --git a/src/Text/Def/Internal/TextLogic/ITextSearchTaggerFactoryService.cs b/src/Text/Def/Internal/TextLogic/ITextSearchTaggerFactoryService.cs new file mode 100644 index 0000000..fa677d4 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/ITextSearchTaggerFactoryService.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations +{ + + using Microsoft.VisualStudio.Text.Tagging; + + /// <summary> + /// Provides <see cref="ITextSearchTagger{T}"/> objects. + /// </summary> + /// <remarks> + /// This class is a Managed Extensibility Framework service provided by the editor. + /// </remarks> + /// <example> + /// [Import] + /// ITextSearchTaggerFactoryService TextSearchTaggerProvider { get; set; } + /// </example> + public interface ITextSearchTaggerFactoryService + { + /// <summary> + /// Creates an <see cref="ITextSearchTagger{T}"/> that searches the <paramref name="buffer"/>. + /// </summary> + /// <typeparam name="T"> + /// The type of tags the tagger will produce. + /// </typeparam> + /// <param name="buffer"> + /// The <see cref="ITextBuffer"/> the tagger will search. + /// </param> + /// <returns> + /// A <see cref="ITextSearchTagger{T}"/> that searches the contents of <paramref name="buffer"/>. + /// </returns> + ITextSearchTagger<T> CreateTextSearchTagger<T>(ITextBuffer buffer) where T : ITag; + } +} diff --git a/src/Text/Def/Internal/TextLogic/PriorityAttribute.cs b/src/Text/Def/Internal/TextLogic/PriorityAttribute.cs new file mode 100644 index 0000000..ab6bc86 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/PriorityAttribute.cs @@ -0,0 +1,35 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// An attribute used to indicate the priority of a named MEF import. If two imports have + /// the same name, then the one with the higher priority wins, + /// </summary> + /// <remarks><para>Currently used only by the EditorFormatMap imports.</para> + /// <para>Default priority is 0 and negative priorities are allowed.</para></remarks> + [global::System.AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)] + public sealed class PriorityAttribute : Attribute + { + /// <summary> + /// Initializes a new instance of <see cref="PriorityAttribute"/>. + /// </summary> + public PriorityAttribute(int priority) + { + this.Priority = priority; + } + + /// <summary> + /// Gets the priority of the export (where an export with the same name and a lower priority will be + /// suppressed by the export with a higher priority). + /// </summary> + public int Priority { get; } + } +} diff --git a/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs b/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs new file mode 100644 index 0000000..77476d3 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/TagAggregatorOptions2.cs @@ -0,0 +1,59 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Tagging +{ + using System; + using Microsoft.VisualStudio.Text.Projection; + + /// <summary> + /// Tag Aggregator options. + /// </summary> + [Flags] + public enum TagAggregatorOptions2 + { + /// <summary> + /// Default behavior. The tag aggregator will map up and down through all projection buffers. + /// </summary> + None = TagAggregatorOptions.None, + + /// <summary> + /// Only map through projection buffers that have the "projection" content type. + /// </summary> + /// <remarks> + /// <para> + /// Normally, a tag aggregator will map up and down through all projection buffers (buffers + /// that implement <see cref="IProjectionBufferBase"/>). This flag will cause the projection buffer + /// to not map through buffers that are projection buffers but do not have a projection content type. + /// </para> + /// </remarks> + /// <comment>This is used by the classifier aggregator, as classification depends on content type.</comment> + MapByContentType = TagAggregatorOptions.MapByContentType, + + /// <summary> + /// Delay creating the taggers for the tag aggregator. + /// </summary> + /// <remarks> + /// <para>A tag aggregator will, normally, create all of its taggers when it is created. This option + /// will cause the tagger to defer the creation until idle time tasks are done.</para> + /// <para>If this option is set, a TagsChanged event will be raised after the taggers have been created.</para> + /// </remarks> + DeferTaggerCreation = 0x02, + + /// <summary> + /// Do not create taggers on child buffers. + /// </summary> + /// <remarks> + /// <para>A common reason to use this flag would for a tagger that is creating its own tag aggregator + /// (for example, to translate one tag into another type of tag). In that case, you can expect another + /// instance of your tagger to be created on the child buffers (which would create its own tag aggregators) + /// so you don't want to have your tag aggregator include those buffers/ + /// </para> + /// </remarks> + NoProjection = 0x04 + } +}
\ No newline at end of file diff --git a/src/Text/Def/Internal/TextLogic/TelemetryEventType.cs b/src/Text/Def/Internal/TextLogic/TelemetryEventType.cs new file mode 100644 index 0000000..39926de --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/TelemetryEventType.cs @@ -0,0 +1,33 @@ +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Supported telemetry event types. + /// </summary> + public enum TelemetryEventType : int + { + /// <summary> + /// User task event + /// </summary> + UserTask = 0, + + /// <summary> + /// Trace event + /// </summary> + Trace = 1, + + /// <summary> + /// Operation event + /// </summary> + Operation = 2, + + /// <summary> + /// Fault event + /// </summary> + Fault = 3, + + /// <summary> + /// Asset event + /// </summary> + Asset = 4, + } +} diff --git a/src/Text/Def/Internal/TextLogic/TelemetryResult.cs b/src/Text/Def/Internal/TextLogic/TelemetryResult.cs new file mode 100644 index 0000000..4f74041 --- /dev/null +++ b/src/Text/Def/Internal/TextLogic/TelemetryResult.cs @@ -0,0 +1,35 @@ +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// An enum to define the result from user task or operation. + /// </summary> + public enum TelemetryResult : int + { + /// <summary> + /// Used for unknown or unavailable result. + /// </summary> + None = 0, + + /// <summary> + /// A result without any failure from product or user. + /// </summary> + Success = 1, + + /// <summary> + /// A result to indicate the action/operation failed because of product issue (not user faults) + /// Consider using FaultEvent to provide more details about the failure. + /// </summary> + Failure = 2, + + /// <summary> + /// A result to indicate the action/operation failed because of user fault (e.g., invalid input). + /// Consider using FaultEvent to provide more details. + /// </summary> + UserFault = 3, + + /// <summary> + /// A result to indicate the action/operation is cancelled by user. + /// </summary> + UserCancel = 4 + } +} diff --git a/src/Text/Def/Internal/TextUI/AdornmentPositioningBehavior2.cs b/src/Text/Def/Internal/TextUI/AdornmentPositioningBehavior2.cs new file mode 100644 index 0000000..7d446a6 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/AdornmentPositioningBehavior2.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Defines the positioning of adornments. + /// </summary> + /// <remarks> + /// This enum adds a mode to the AdornmentPositioningBehavior needed for diff but we don't want to expose. + /// </remarks> + public enum AdornmentPositioningBehavior2 + { + /// <summary> + /// The adornment is not moved automatically. + /// </summary> + OwnerControlled = AdornmentPositioningBehavior.OwnerControlled, + + /// <summary> + /// The adornment is positioned relative to the top left corner of the view. + /// </summary> + ViewportRelative = AdornmentPositioningBehavior.ViewportRelative, + + /// <summary> + /// The adornment is positioned relative to the text in the view. + /// </summary> + TextRelative = AdornmentPositioningBehavior.TextRelative, + + /// <summary> + /// Behaves like a AdornmentPositioningBehavior.TextRelative adornment but only scrolls vertically. + /// </summary> + TextRelativeVerticalOnly + } +}
\ No newline at end of file diff --git a/src/Text/Def/Internal/TextUI/Caret.cs b/src/Text/Def/Internal/TextUI/Caret.cs new file mode 100644 index 0000000..e6faab3 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/Caret.cs @@ -0,0 +1,237 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + using System; + + /// <summary> + /// Manipulates the on-screen caret in the editor. + /// </summary> + public abstract class Caret : DisplayTextPoint + { + /// <summary> + /// When implemented in a derived class, moves the caret to the next character. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToNextCharacter(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the previous character. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToPreviousCharacter(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the beginning of the previous line in the buffer. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToBeginningOfPreviousLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the beginning of the next line in the buffer. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToBeginningOfNextLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the beginning of the previous line in the view. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + /// <remarks> + /// If the caret is on the first line of the file, the caret is moved to the beginning of the line. + /// </remarks> + public abstract void MoveToBeginningOfPreviousViewLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the beginning of the next line in the view. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + /// <remarks> + /// If the caret is on the last line of the file, the caret is moved to the end of the line. + /// </remarks> + public abstract void MoveToBeginningOfNextViewLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret one line up, preserving its horizontal position. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToPreviousLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret one line down, preserving its horizontal position. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToNextLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret one page up. + /// </summary> + public abstract void MovePageUp(); + + /// <summary> + /// When implemented in a derived class, moves the caret one page down. + /// </summary> + public abstract void MovePageDown(); + + /// <summary> + /// When implemented in a derived class, moves the caret one page up. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MovePageUp(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret one page down. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MovePageDown(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the end of the line in the buffer. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToEndOfLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the start of the line in the buffer. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToStartOfLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the end of the line in the view. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToEndOfViewLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the start of the line in the view. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToStartOfViewLine(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the position and optionally extends the selection + /// if necessary. + /// </summary> + /// <param name="position"> + /// The position to place the caret. + /// </param> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than 0 or greater than the line number of the last line in the TextBuffer.</exception> + public abstract void MoveTo(int position, bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the start of the specified line. + /// </summary> + /// <param name="lineNumber"> + /// The line number to which to move the caret. + /// </param> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than 0 or greater than the line number of the last line in the TextBuffer.</exception> + public abstract void MoveToLine(int lineNumber, bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to an offset from the start of the specified line. + /// </summary> + /// <param name="lineNumber"> + /// The line number to which to move the caret. + /// </param> + /// <param name="offset"> + /// The number of characters from the start of the line at which the caret should be moved. + /// </param> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + /// <remarks>If <paramref name="offset"/> exceeds the length of the line and virtual space is enabled, the caret will be + /// positioned in virtual space. Otherwise the caret will be placed at the end of the line.</remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than zero + /// or greater than the line number of the last line in the text buffer, or + /// <paramref name="offset"/> is less than zero.</exception> + public abstract void MoveToLine(int lineNumber, int offset, bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the start of the document. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToStartOfDocument(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the end of the document. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToEndOfDocument(bool extendSelection); + + /// <summary> + /// When implemented in a derived class,m oves the caret to the end of the current word, or to the beginning of the + /// next word if it is already at the end of the current word. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToNextWord(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, moves the caret to the start of the current word, or to the end of the + /// previous word if it is already at the start of the current word. + /// </summary> + /// <param name="extendSelection"> + /// If <c>true</c>, the selection is extended when the caret is moved; if <c>false</c>, the selection is not extended. + /// </param> + public abstract void MoveToPreviousWord(bool extendSelection); + + /// <summary> + /// When implemented in a derived class, ensures that the caret is visible on the screen. + /// </summary> + public abstract void EnsureVisible(); + + /// <summary> + /// When implemented in a derived class, gets advanced caret functionality. + /// </summary> + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)] + public abstract ITextCaret AdvancedCaret + { + get; + } + } +} diff --git a/src/Text/Def/Internal/TextUI/DisplayTextPoint.cs b/src/Text/Def/Internal/TextUI/DisplayTextPoint.cs new file mode 100644 index 0000000..74302ea --- /dev/null +++ b/src/Text/Def/Internal/TextUI/DisplayTextPoint.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; +using Microsoft.VisualStudio.Text.Formatting; + +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Represents a point in the <see cref="TextBuffer"/> that behaves relative to the view in which it lives. + /// </summary> + /// <remarks> + /// <para> + /// While this point is immutable, its position may change in response + /// to edits in the text. + /// </para> + /// <para> + /// The start point is always before the end point. + /// </para> + /// </remarks> + public abstract class DisplayTextPoint : TextPoint + { + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextView"/> of this point. + /// </summary> + public abstract TextView TextView { get; } + + /// <summary> + /// When implemented in a derived class, gets the <see cref="ITextViewLine"/> that contains this point. + /// </summary> + public abstract ITextViewLine AdvancedTextViewLine { get; } + + /// <summary> + /// When implemented in a derived class, gets the position of the start of the line in the TextView that this DisplayTextPoint is on. + /// </summary> + /// <remarks>This value could be affected by whether or not Word Wrap is turned on in the view.</remarks> + public abstract int StartOfViewLine { get; } + + /// <summary> + /// When implemented in a derived class, gets the position of the end of the line in the TextView that this DisplayTextPoint is on. + /// </summary> + /// <remarks>This value could be affected by whether or not Word Wrap is turned on in the view.</remarks> + public abstract int EndOfViewLine { get; } + + /// <summary> + /// When implemented in a derived class, moves this point to the end of the line in the TextView that it is currently on. + /// </summary> + /// <remarks>This value could be affected by whether or not Word Wrap is turned on in the view.</remarks> + public abstract void MoveToEndOfViewLine(); + + /// <summary> + /// When implemented in a derived class, moves this point to the start of the line in the TextView that it is currently on. + /// </summary> + /// <remarks>This value could be affected by whether or not Word Wrap is turned on in the view.</remarks> + public abstract void MoveToStartOfViewLine(); + + /// <summary> + /// When implemented in a derived class, moves this point to the beginning of the next line in the TextView. + /// </summary> + /// <remarks> + /// <para>This point moves to the end of the line if the point is on the last + /// line.</para> + /// <para>This value could be affected by whether or not Word Wrap is turned on in the view.</para> + /// <para>If the point is on the last line of the file, the caret is moved to the end of the line.</para> + /// </remarks> + public abstract void MoveToBeginningOfNextViewLine(); + + /// <summary> + /// When implemented in a derived class, moves this point to the beginning of the previous line in the TextView. + /// </summary> + /// <remarks> + /// <para>This point moves to the start of the line if the point is on the first + /// line.</para> + /// <para>This value could be affected by whether or not Word Wrap is turned on in the view.</para> + /// <para>If the point is on the first line of the file, the caret is moved to the beginning of the line.</para> + /// </remarks> + public abstract void MoveToBeginningOfPreviousViewLine(); + + /// <summary> + /// When implemented in a derived class, gets a display text point for the first + /// non-whitespace character on the current view line. + /// </summary> + /// <remarks> + /// If a line is all white space, this method returns a <see cref="DisplayTextPoint"/> at the start of the line. + /// </remarks> + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)] + public abstract DisplayTextPoint GetFirstNonWhiteSpaceCharacterOnViewLine(); + + /// <summary> + /// When implemented in a derived class, gets the integer representation of the current position of this text point + /// in relation to the visual start of the line. + /// </summary> + public abstract int DisplayColumn { get; } + + /// <summary> + /// When implemented in a derived class, determines whether the point is currently visible on the screen. + /// </summary> + public abstract bool IsVisible { get; } + + /// <summary> + /// Creates a new <see cref="DisplayTextPoint"/> at this position that can be + /// moved independently from this one. + /// </summary> + new public DisplayTextPoint Clone() + { + return CloneDisplayTextPointInternal(); + } + + /// <summary> + /// When implemented in a derived class, gets the <see cref="DisplayTextRange"/> that has this point and <paramref name="otherPoint"/> + /// as its start and end points. + /// </summary> + /// <returns>The <see cref="DisplayTextRange"/> that starts at this point and ends at <paramref name="otherPoint"/>.</returns> + /// <exception cref="InvalidOperationException"><paramref name="otherPoint"/> does not belong to the same buffer as this point, or + /// <paramref name="otherPoint"/> does not belong to the same view as this point.</exception> + public abstract DisplayTextRange GetDisplayTextRange(DisplayTextPoint otherPoint); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="DisplayTextRange"/> that has this point and <paramref name="otherPosition"/> + /// as its start and end positions. + /// </summary> + /// <returns>The <see cref="DisplayTextRange"/> that starts at this point and ends at <paramref name="otherPosition"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="otherPosition"/> is in negative or past the end of this buffer.</exception> + public abstract DisplayTextRange GetDisplayTextRange(int otherPosition); + + /// <summary> + /// Clones this <see cref="DisplayTextPoint"/>. + /// </summary> + /// <returns></returns> + protected sealed override TextPoint CloneInternal() + { + return CloneDisplayTextPointInternal(); + } + + /// <summary> + /// When implemented in a derived class, clones this <see cref="DisplayTextPoint"/>. + /// </summary> + protected abstract DisplayTextPoint CloneDisplayTextPointInternal(); + } +} diff --git a/src/Text/Def/Internal/TextUI/DisplayTextRange.cs b/src/Text/Def/Internal/TextUI/DisplayTextRange.cs new file mode 100644 index 0000000..9becda5 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/DisplayTextRange.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System.Collections; +using System.Collections.Generic; +using Microsoft.VisualStudio.Text.Formatting; + +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Represents a range in the <see cref="TextBuffer"/> that behaves relative to the view in which it lives. + /// </summary> + public abstract class DisplayTextRange : TextRange, IEnumerable<DisplayTextPoint> + { + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextView"/> of this range. + /// </summary> + public abstract TextView TextView { get; } + + /// <summary> + /// Creates a clone of this text range than can be moved independently of this one. + /// </summary> + public new DisplayTextRange Clone() + { + return CloneDisplayTextRangeInternal(); + } + + /// <summary> + /// When implemented in a derived class, gets the start point of this text range. + /// </summary> + public abstract DisplayTextPoint GetDisplayStartPoint(); + + /// <summary> + /// When implemented in a derived class, gets the end point of this text range. + /// </summary> + public abstract DisplayTextPoint GetDisplayEndPoint(); + + /// <summary> + /// When implemented in a derived class, gets the visibility state of this text range. + /// </summary> + public abstract VisibilityState Visibility { get; } + + /// <summary> + /// Clones this text range. + /// </summary> + /// <returns>The cloned <see cref="TextRange"/>.</returns> + protected override TextRange CloneInternal() + { + return CloneDisplayTextRangeInternal(); + } + + /// <summary> + /// When implemented in a derived class, clones the <see cref="DisplayTextRange"/>. + /// </summary> + protected abstract DisplayTextRange CloneDisplayTextRangeInternal(); + + /// <summary> + /// When implemented in a derived class, gets the enumerator of type <see cref="DisplayTextPoint"/>. + /// </summary> + /// <returns></returns> + protected abstract IEnumerator<DisplayTextPoint> GetDisplayPointEnumeratorInternal(); + + #region IEnumerable<DisplayTextPoint> Members + + /// <summary> + /// Gets an enumerator of type <see cref="DisplayTextPoint"/>. + /// </summary> + /// <returns></returns> + public new IEnumerator<DisplayTextPoint> GetEnumerator() + { + return GetDisplayPointEnumeratorInternal(); + } + + #endregion + + #region IEnumerable Members + + /// <summary> + /// Gets the enumerator. + /// </summary> + /// <returns></returns> + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/src/Text/Def/Internal/TextUI/HowToShow.cs b/src/Text/Def/Internal/TextUI/HowToShow.cs new file mode 100644 index 0000000..4dec626 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/HowToShow.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Defines the ways to display a point or range. + /// </summary> + public enum HowToShow + { + /// <summary> + /// Show the point or start of the range as it is on screen, or scroll the + /// view the minimal amount in order to bring the point or range into view. + /// </summary> + AsIs, + + /// <summary> + /// Show the point or start of the range centered on the screen. + /// </summary> + Centered, + + /// <summary> + /// Show the point or start of the range on the first line of the view. + /// </summary> + OnFirstLineOfView, + } +} diff --git a/src/Text/Def/Internal/TextUI/IAccurateOutliningManager.cs b/src/Text/Def/Internal/TextUI/IAccurateOutliningManager.cs new file mode 100644 index 0000000..6acecbe --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IAccurateOutliningManager.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Outlining +{ + using System; + using System.Collections.Generic; + using System.Threading; + + /// <summary> + /// Extension of IOutliningManager to allow it to get accurate (if slow) results from the outlining taggers. + /// </summary> + /// <remarks> + /// <para>This interface only contains the minimal number of overloads of IOutliningManager methods to make restoring regions when opening a file + /// work. More overloads can be added as needed.</para> + /// </remarks> + public interface IAccurateOutliningManager : IOutliningManager + { + #region Collapsing and Expanding + + /// <summary> + /// Collapses all regions that match the specified predicate. + /// </summary> + /// <param name="span">The regions that intersect this span.</param> + /// <param name="match">The predicate to match.</param> + /// <returns>The newly-collapsed regions.</returns> + /// <remarks> + /// The <paramref name="match"/> predicate may be passed regions that cannot actually be collapsed, due + /// to the region being partially obscured by another already collapsed region (either pre-existing or collapsed + /// in an earlier call to the predicate). The elements of the returned enumeration do accurately track + /// the regions that were collapsed, so they may differ from the elements for which the predicate returned <c>true</c>. + /// </remarks> + IEnumerable<ICollapsed> CollapseAll(SnapshotSpan span, Predicate<ICollapsible> match, CancellationToken cancel); + + #endregion + } +} diff --git a/src/Text/Def/Internal/TextUI/IAnnotationTag.cs b/src/Text/Def/Internal/TextUI/IAnnotationTag.cs new file mode 100644 index 0000000..bd824df --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IAnnotationTag.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + using Microsoft.VisualStudio.Text.Tagging; + + public interface IAnnotationTag : ITag + { + /// <summary> + /// Can the user navigate to the location of this item (errors, find matches, collapsed regions). + /// </summary> + bool IsNavigable { get; } + + /// <summary> + /// Some unique object where things of the same type (e.g. tracepoints) return the same object. Used to group similar things together. + /// </summary> + AnnotationKind ItemKindIdentifier { get; } + + /// <summary> + /// What should be read out to indicate the existance of <paramref name="count"/> things of the same kind (e.g. "1 warning", or "2 errors"). + /// </summary> + string ItemKindDisplayText(int count); + } + + public abstract class AnnotationTag : IAnnotationTag + { + public virtual bool IsNavigable => false; + + public abstract AnnotationKind ItemKindIdentifier { get; } + + public abstract string ItemKindDisplayText(int count); + } + + public enum AnnotationKind + { + Error = 1000, + Warning = 2000, + Message = 3000, + InstructionPointer = 3500, + Breakpoint = 4000, + Shortcut = 5000, + Tracepoint = 6000, + Bookmark = 7000, + CollapsedRegion = 8000, + ExpandedRegion = 9000, + Suggestion = 10000, + LineAddition = 11000, + LineDeletion = 12000, + WordChange = 13000 + } +} diff --git a/src/Text/Def/Internal/TextUI/IBraceCompletionManager.cs b/src/Text/Def/Internal/TextUI/IBraceCompletionManager.cs new file mode 100644 index 0000000..cb633af --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IBraceCompletionManager.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.BraceCompletion +{ + /// <summary> + /// A per text view manager for brace completion. + /// </summary> + public interface IBraceCompletionManager + { + /// <summary> + /// Returns true if brace completion is enabled. + /// </summary> + bool Enabled { get; } + + /// <summary> + /// Returns true if there are currently active sessions. + /// </summary> + bool HasActiveSessions { get; } + + /// <summary> + /// Opening brace characters the brace completion manager is currently registered to handle. + /// </summary> + string OpeningBraces { get; } + + /// <summary> + /// Closing brace characters the brace completion manager is currently registered to handle. + /// </summary> + string ClosingBraces { get; } + + /// <summary> + /// Called by the editor when a character has been typed and before it is + /// inserted into the buffer. + /// </summary> + /// <param name="handledCommand">Set to true to prevent the closing brace character from being + /// inserted into the buffer.</param> + void PreTypeChar(char character, out bool handledCommand); + + /// <summary> + /// Called by the editor after a character has been typed. + /// </summary> + void PostTypeChar(char character); + + /// <summary> + /// Called by the editor when tab has been pressed and before it is inserted into the buffer. + /// </summary> + /// <param name="handledCommand">Set to true to prevent the tab from being inserted into the buffer.</param> + void PreTab(out bool handledCommand); + + /// <summary> + /// Called by the editor after the tab has been inserted. + /// </summary> + void PostTab(); + + /// <summary> + /// Called by the editor before the character has been removed. + /// </summary> + /// <param name="handledCommand">Set to true to prevent the backspace action from completing.</param> + void PreBackspace(out bool handledCommand); + + /// <summary> + /// Called by the editor after the character has been removed. + /// </summary> + void PostBackspace(); + + /// <summary> + /// Called by the editor when delete is pressed within the session. + /// </summary> + /// <param name="handledCommand">Set to true to prevent the deletion.</param> + void PreDelete(out bool handledCommand); + + /// <summary> + /// Called by the editor after the delete action. + /// </summary> + void PostDelete(); + + /// <summary> + /// Called by the editor when return is pressed within the session. + /// </summary> + /// <param name="handledCommand">Set to true to prevent the new line insertion.</param> + void PreReturn(out bool handledCommand); + + /// <summary> + /// Called by the editor after the new line has been inserted. + /// </summary> + void PostReturn(); + + } +} diff --git a/src/Text/Def/Internal/TextUI/IBufferPrimitives.cs b/src/Text/Def/Internal/TextUI/IBufferPrimitives.cs new file mode 100644 index 0000000..9d951a1 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IBufferPrimitives.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Represents common buffer primitives and an extensible mechanism for replacing their values and adding new options. + /// </summary> + public interface IBufferPrimitives + { + /// <summary> + /// Gets the <see cref="TextBuffer"/> primitive used for text manipulation. + /// </summary> + TextBuffer Buffer { get; } + } +} diff --git a/src/Text/Def/Internal/TextUI/IBufferPrimitivesFactoryService.cs b/src/Text/Def/Internal/TextUI/IBufferPrimitivesFactoryService.cs new file mode 100644 index 0000000..20dec97 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IBufferPrimitivesFactoryService.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Creates buffer primitives. + /// </summary> + /// <remarks> + /// <para> + /// This factory is designed to be used by other primitives. Consumers of the primitives + /// should use the <see cref="IEditorPrimitivesFactoryService"/> to get a reference to a <see cref="TextBuffer"/> + /// and use that to create the primitives. + /// </para> + /// </remarks> + /// <remarks>This is a MEF component part, and should be imported as follows: + /// [Import] + /// IBufferPrimitivesFactoryService factory = null; + /// </remarks> + public interface IBufferPrimitivesFactoryService + { + /// <summary> + /// Creates a <see cref="TextBuffer"/> primitive. + /// </summary> + /// <param name="textBuffer">The <see cref="ITextBuffer"/> on which to base this primitive.</param> + /// <returns>The <see cref="TextBuffer"/> primitive for the given <see cref="ITextBuffer"/>.</returns> + /// <remarks> + /// <para> + /// This method always returns the same object if the same <see cref="ITextBuffer"/> is passed in. + /// </para> + /// </remarks> + TextBuffer CreateTextBuffer(ITextBuffer textBuffer); + + /// <summary> + /// Creates a new <see cref="TextPoint"/> primitive. + /// </summary> + /// <param name="textBuffer">The <see cref="TextBuffer"/> to which this <see cref="TextPoint"/> belongs.</param> + /// <param name="position">The position of this <see cref="TextPoint"/>.</param> + /// <returns>A new <see cref="TextPoint"/> primitive at the given <paramref name="position"/>.</returns> + TextPoint CreateTextPoint(TextBuffer textBuffer, int position); + + /// <summary> + /// Creates a new <see cref="TextRange"/> primitive. + /// </summary> + /// <param name="textBuffer">The <see cref="TextBuffer"/> to which this <see cref="TextPoint"/> belongs.</param> + /// <param name="startPoint">The <see cref="TextPoint"/> of the start.</param> + /// <param name="endPoint">The <see cref="TextPoint"/> of the end.</param> + /// <returns>A new <see cref="TextRange"/> primitive at the given <paramref name="startPoint"/> and <paramref name="endPoint"/>.</returns> + TextRange CreateTextRange(TextBuffer textBuffer, TextPoint startPoint, TextPoint endPoint); + } +} diff --git a/src/Text/Def/Internal/TextUI/IEditorPrimitivesFactoryService.cs b/src/Text/Def/Internal/TextUI/IEditorPrimitivesFactoryService.cs new file mode 100644 index 0000000..d842b50 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IEditorPrimitivesFactoryService.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// A service that provides primitives for a given <see cref="ITextView"/> or <see cref="ITextBuffer"/>. + /// </summary> + /// <remarks>This is a MEF component part, and should be imported as follows: + /// [Import] + /// IEditorPrimitivesFactoryService factory = null; + /// </remarks> + public interface IEditorPrimitivesFactoryService + { + /// <summary> + /// Gets the <see cref="IViewPrimitives"/> for the <see cref="ITextView"/>. + /// </summary> + /// <param name="textView">The <see cref="ITextView"/> for which to get the <see cref="IViewPrimitives"/>.</param> + /// <returns>The <see cref="IViewPrimitives"/> for the given <see cref="ITextView"/>.</returns> + IViewPrimitives GetViewPrimitives(ITextView textView); + + /// <summary> + /// Gets the <see cref="IBufferPrimitives"/> for the <see cref="ITextBuffer"/>. + /// </summary> + /// <param name="textBuffer">The <see cref="ITextBuffer"/> for which to fetch <see cref="IBufferPrimitives"/>.</param> + /// <returns>The <see cref="IBufferPrimitives"/> for the given <see cref="ITextBuffer"/>.</returns> + IBufferPrimitives GetBufferPrimitives(ITextBuffer textBuffer); + } +} diff --git a/src/Text/Def/Internal/TextUI/IMapEditToData.cs b/src/Text/Def/Internal/TextUI/IMapEditToData.cs new file mode 100644 index 0000000..989edc5 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IMapEditToData.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + + public interface IMapEditToData + { + int MapEditToData(int editPoint); + int MapDataToEdit(int dataPoint); + } +} diff --git a/src/Text/Def/Internal/TextUI/IObscuringTip.cs b/src/Text/Def/Internal/TextUI/IObscuringTip.cs new file mode 100644 index 0000000..d0f2c46 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IObscuringTip.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + public interface IObscuringTip + { + /// <summary> + /// Dismiss the tip. Return true if the tip had been visible. + /// </summary> + bool Dismiss(); + + /// <summary> + /// Get the current opacity of the tip (should be 100% unless explicitly set otherwise). + /// </summary> + double Opacity { get; } + + /// <summary> + /// Set the opacity of the tip (generally to either 100% or 10% while the control key is held down. + /// </summary> + void SetOpacity(double opacity); + } + + public abstract class Tip : IObscuringTip + { + public abstract bool Dismiss(); + + public virtual double Opacity => 1.0; + + public virtual void SetOpacity(double opacity) { } + } +} diff --git a/src/Text/Def/Internal/TextUI/IObscuringTipManager.cs b/src/Text/Def/Internal/TextUI/IObscuringTipManager.cs new file mode 100644 index 0000000..43e8de5 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IObscuringTipManager.cs @@ -0,0 +1,19 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Class used to manage tips displayed in a view. + /// </summary> + public interface IObscuringTipManager + { + void PushTip(ITextView view, IObscuringTip tip); + + void RemoveTip(ITextView view, IObscuringTip tip); + } +} diff --git a/src/Text/Def/Internal/TextUI/IStructureTipManager.cs b/src/Text/Def/Internal/TextUI/IStructureTipManager.cs new file mode 100644 index 0000000..eb926d8 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IStructureTipManager.cs @@ -0,0 +1,25 @@ +namespace Microsoft.VisualStudio.Text.Structure +{ + using Microsoft.VisualStudio.Text.Editor; + + /// <summary> + /// Facilitates invocation of Structure Guide Lines tooltip. + /// </summary> + public interface IStructureTipManager + { + /// <summary> + /// Gets whether or not Structure Tips are available in the current view. + /// </summary> + /// <param name="textView">The current view.</param> + /// <returns>Returns true if structure tips are available.</returns> + bool CanTriggerStructureTip(ITextView textView); + + /// <summary> + /// Displays the structure guide lines tooltip containing the context at the + /// specified trigger point. + /// </summary> + /// <param name="textView">The textview to display the tip for.</param> + /// <param name="point">The point to display context for.</param> + void TriggerStructureTip(ITextView textView, SnapshotPoint point); + } +} diff --git a/src/Text/Def/Internal/TextUI/ITextView2.cs b/src/Text/Def/Internal/TextUI/ITextView2.cs new file mode 100644 index 0000000..38ff665 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/ITextView2.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + using System; + + /// <summary> + /// An extension of the ITextView that exposes some internal hooks. + /// </summary> + public interface ITextView2 : ITextView + { + /// <summary> + /// The MaxTextRightCoordinate of the view based only on the text contained in the view. + /// </summary> + double RawMaxTextRightCoordinate + { + get; + } + + /// <summary> + /// The minimum value for the view's MaxTextRightCoordinate. + /// </summary> + /// <remarks> + /// If setting this value changes the view's MaxTextRightCoordinate, the view will raise a layout changed event. + /// </remarks> + double MinMaxTextRightCoordinate + { + get; + set; + } + + /// <summary> + /// Raised whenever the view's MaxTextRightCoordinate is changed. + /// </summary> + /// <remarks> + /// This event will only be rasied if the MaxTextRightCoordinate is changed by changing the MinMaxTextRightCoordinate property + /// (it will not be raised as a side-effect of a layout even if the layout does change the MaxTextRightCoordinate). + /// </remarks> + event EventHandler MaxTextRightCoordinateChanged; + } +} diff --git a/src/Text/Def/Internal/TextUI/IThumbnailSupport.cs b/src/Text/Def/Internal/TextUI/IThumbnailSupport.cs new file mode 100644 index 0000000..ad640eb --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IThumbnailSupport.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + public interface IThumbnailSupport + { + /// <summary> + /// Controls whether or note the view's visuals are removed when the view is hidden. + /// </summary> + /// <remarks> + /// Defaults to true and is should to false when generating thumbnails of a hidden view (then restored afterwards). + /// </remarks> + bool RemoveVisualsWhenHidden { get; set; } + } +} diff --git a/src/Text/Def/Internal/TextUI/IViewPrimitives.cs b/src/Text/Def/Internal/TextUI/IViewPrimitives.cs new file mode 100644 index 0000000..4130715 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IViewPrimitives.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + + /// <summary> + /// Represents common view primitives and an extensible mechanism for replacing their values and adding new options. + /// </summary> + public interface IViewPrimitives : IBufferPrimitives + { + /// <summary> + /// Gets the <see cref="View"/> primitive used for scrolling the editor window. + /// </summary> + TextView View { get; } + + /// <summary> + /// Gets the <see cref="Selection"/> primitive used for selection manipulation. + /// </summary> + Selection Selection { get; } + + /// <summary> + /// Gets the <see cref="Caret"/> primitive used for caret movement. + /// </summary> + Caret Caret { get; } + } + + /// <summary> + /// Represents the common editor primitives produced by this subsystem. + /// </summary> + public static class EditorPrimitiveIds + { + /// <summary> + /// The ID for the view. + /// </summary> + public const string ViewPrimitiveId = "Editor.View"; + + /// <summary> + /// The ID for the selection. + /// </summary> + public const string SelectionPrimitiveId = "Editor.Selection"; + + /// <summary> + /// The ID for the caret. + /// </summary> + public const string CaretPrimitiveId = "Editor.Caret"; + + /// <summary> + /// The ID for the buffer. + /// </summary> + public const string BufferPrimitiveId = "Editor.TextBuffer"; + } +} diff --git a/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs b/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs new file mode 100644 index 0000000..2408671 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/IViewPrimitivesFactoryService.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Creates view primitives. + /// </summary> + /// <remarks> + /// <para> + /// This factory is designed to be used by other primitives. Consumers of the primitives + /// should use the <see cref="IEditorPrimitivesFactoryService"/> to get a reference to a + /// <see cref="TextView"/> and use that to create the primitives. + /// </para> + /// </remarks> + /// <remarks>This is a MEF component part, and should be imported as follows: + /// [Import] + /// IViewPrimitivesFactoryService factory = null; + /// </remarks> + public interface IViewPrimitivesFactoryService + { + /// <summary> + /// Creates a <see cref="TextView"/> primitive. + /// </summary> + /// <param name="textView">The <see cref="ITextView"/> on which to base this primitive.</param> + /// <returns>The <see cref="TextView"/> primitive for the given <see cref="ITextView"/>.</returns> + /// <remarks> + /// <para> + /// This method always returns the same object if the same <see cref="ITextView"/> is passed in. + /// </para> + /// </remarks> + TextView CreateTextView(ITextView textView); + + /// <summary> + /// Creates a new <see cref="DisplayTextPoint"/> primitive. + /// </summary> + /// <param name="textView">The <see cref="TextView"/> to which this <see cref="DisplayTextPoint"/> belongs.</param> + /// <param name="position">The position of this <see cref="DisplayTextPoint"/>.</param> + /// <returns>A new <see cref="DisplayTextPoint"/> primitive at the given <paramref name="position"/>.</returns> + DisplayTextPoint CreateDisplayTextPoint(TextView textView, int position); + + /// <summary> + /// Creates a new <see cref="DisplayTextRange"/> primitive. + /// </summary> + /// <param name="textView">The <see cref="TextView"/> to which this <see cref="DisplayTextRange"/> belongs.</param> + /// <param name="textRange">The <see cref="TextRange"/> in the <see cref="TextBuffer"/>.</param> + /// <returns>A new <see cref="DisplayTextRange"/> primitive at the given <paramref name="textView"/> and <paramref name="textRange"/>.</returns> + DisplayTextRange CreateDisplayTextRange(TextView textView, TextRange textRange); + + /// <summary> + /// Creates a <see cref="Selection"/> primitive. + /// </summary> + /// <param name="textView">The <see cref="ITextView"/> on which to base this primitive.</param> + /// <returns>The <see cref="Selection"/> primitive for the given <see cref="ITextView"/>.</returns> + /// <remarks> + /// <para> + /// This method always returns the same object if the same <see cref="ITextView"/> is passed in. + /// </para> + /// </remarks> + Selection CreateSelection(TextView textView); + + /// <summary> + /// Creates a <see cref="Caret"/> primitive. + /// </summary> + /// <param name="textView">The <see cref="ITextView"/> on which to base this primitive.</param> + /// <returns>The <see cref="Caret"/> primitive for the given <see cref="ITextView"/>.</returns> + /// <remarks> + /// <para> + /// This method always returns the same object if the same <see cref="ITextView"/> is passed in. + /// </para> + /// </remarks> + Caret CreateCaret(TextView textView); + } +} diff --git a/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs b/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs new file mode 100644 index 0000000..3cff2ac --- /dev/null +++ b/src/Text/Def/Internal/TextUI/OverviewFormatDefinitions.cs @@ -0,0 +1,17 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.OverviewMargin +{ + public static class OverviewFormatDefinitions + { + public const string ElisionColorName = "OverviewMarginCollapsedRegion"; + public const string OffScreenColorName = "OverviewMarginBackground"; + public const string VisibleColorName = "OverviewMarginVisible"; + public const string CaretColorName = "OverviewMarginCaret"; + } +} diff --git a/src/Text/Def/Internal/TextUI/Selection.cs b/src/Text/Def/Internal/TextUI/Selection.cs new file mode 100644 index 0000000..59d27eb --- /dev/null +++ b/src/Text/Def/Internal/TextUI/Selection.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + using System; + + /// <summary> + /// Represents the selection on the screen. + /// </summary> + public abstract class Selection : DisplayTextRange + { + /// <summary> + /// When implemented in a derived class, selects the given text range. + /// </summary> + /// <param name="textRange">The range to select.</param> + public abstract void SelectRange(TextRange textRange); + + /// <summary> + /// When implemented in a derived class, selects the given text range, reversing the selection if needed. Ensures + /// that the end point of the selection is visible on screen. + /// </summary> + /// <param name="selectionStart">The start point for the selection.</param> + /// <param name="selectionEnd">The end point for the selection.</param> + /// <remarks>If <paramref name="selectionStart"/> is positioned after <paramref name="selectionEnd"/>, then the + /// selection is reversed.</remarks> + public abstract void SelectRange(TextPoint selectionStart, TextPoint selectionEnd); + + /// <summary> + /// When implemented in a derived class, selects all the text in the document. Ensures that the end point + /// of the selection is visible on screen. + /// </summary> + public abstract void SelectAll(); + + /// <summary> + /// When implemented in a derived class, extends the selection from its current start point to the new end point. Ensures + /// that the end point of the selection is visible on screen. + /// </summary> + /// <param name="newEnd"> + /// The text point to which to extend the selection. + /// </param> + /// <remarks> + /// <paramref name="newEnd"/> may become the new start point, if <paramref name="newEnd"/> is before the current start point. + /// </remarks> + /// <exception cref="InvalidOperationException"><paramref name="newEnd"/> belongs to a different buffer.</exception> + public abstract void ExtendSelection(TextPoint newEnd); + + /// <summary> + /// When implemented in a derived class, resets any selection in the text. + /// </summary> + public abstract void Clear(); + + /// <summary> + /// When implemented in a derived class, provides advanced selection manipulation functionality. + /// </summary> + /// <returns>The <see cref="ITextSelection"/>.</returns> + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)] + public abstract ITextSelection AdvancedSelection + { + get; + } + + /// <summary> + /// When implemented in a derived class, determines whether the end point represents the start of the selection. + /// </summary> + public abstract bool IsReversed + { + get; + set; + } + } +} diff --git a/src/Text/Def/Internal/TextUI/TextBuffer.cs b/src/Text/Def/Internal/TextUI/TextBuffer.cs new file mode 100644 index 0000000..8343b5d --- /dev/null +++ b/src/Text/Def/Internal/TextUI/TextBuffer.cs @@ -0,0 +1,103 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + using System; + using System.Collections.Generic; + + /// <summary> + /// Provides methods for text insertion, deletion, and modification. + /// </summary> + public abstract class TextBuffer + { + /// <summary> + /// When implemented in a derived class, gets a text point from a integer position. + /// </summary> + /// <param name="position">The position at which to get the text point.</param> + /// <returns>The <see cref="TextPoint"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is negative or past the end of the buffer.</exception> + public abstract TextPoint GetTextPoint(int position); + + /// <summary> + /// When implemented in a derived class, gets a text point from a line and column. + /// </summary> + /// <param name="line">The line on which to get this text point.</param> + /// <param name="column">The line-relative position at which to get the text point.</param> + /// <returns>The <see cref="TextPoint"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="line"/> is negative or greater than the number of lines in the buffer, or + /// <paramref name="column"/> is negative or past the end of the line.</exception> + /// <remarks> + /// The line and column are zero-based. + /// </remarks> + public abstract TextPoint GetTextPoint(int line, int column); + + /// <summary> + /// When implemented in a derived class, gets a <see cref="TextRange"/> representing a line in the <see cref="TextBuffer"/>. + /// </summary> + /// <param name="line">The line.</param> + /// <returns>A <see cref="TextRange"/> representing a line in the <see cref="TextBuffer"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="line"/> is negative or greater than the number of lines in the buffer.</exception> + /// <remarks> + /// <para> + /// The <see cref="TextRange"/> returned does not include the line break characters for the line. + /// </para> + /// </remarks> + public abstract TextRange GetLine(int line); + + /// <summary> + /// When implemented in a derived class, gets a text range from two text points. + /// </summary> + /// <param name="startPoint">The start point of the range.</param> + /// <param name="endPoint">The end point of the range.</param> + /// <returns>The text range that starts and ends at the two points.</returns> + /// <remarks>The start point of the text range may become the end point if the start point is after the end point.</remarks> + /// <exception cref="InvalidOperationException"><paramref name="startPoint"/> or <paramref name="endPoint"/> do not belong to this buffer.</exception> + public abstract TextRange GetTextRange(TextPoint startPoint, TextPoint endPoint); + + /// <summary> + /// When implemented in a derived class, gets a text range from two integer positions. + /// </summary> + /// <param name="startPosition">The start position of the range.</param> + /// <param name="endPosition">The end position of the range.</param> + /// <returns>The text range that starts and ends at the two positions.</returns> + /// <remarks>The start position of the text range may become the end point if the start position is after the end position.</remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="startPosition"/> is negative or past the end of the buffer.</exception> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="endPosition"/> is negative or past the end of the buffer.</exception> + public abstract TextRange GetTextRange(int startPosition, int endPosition); + + /// <summary> + /// When implemented in a derived class, provides advanced text manipulation functionality. + /// </summary> + /// <returns>The <see cref="ITextBuffer"/>.</returns> + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)] + public abstract ITextBuffer AdvancedTextBuffer + { + get; + } + + /// <summary> + /// When implemented in a derived class, gets the start point of the buffer (always zero). + /// </summary> + /// <returns>The starting <see cref="TextPoint"/>.</returns> + public abstract TextPoint GetStartPoint(); + + /// <summary> + /// When implemented in a derived class, gets the end point of the buffer (always the last position in the buffer. + /// </summary> + /// <returns>The end <see cref="TextPoint"/>.</returns> + public abstract TextPoint GetEndPoint(); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextRange"/> objectss representing lines in the buffer. + /// </summary> + public abstract IEnumerable<TextRange> Lines + { + get; + } + } +} diff --git a/src/Text/Def/Internal/TextUI/TextPoint.cs b/src/Text/Def/Internal/TextUI/TextPoint.cs new file mode 100644 index 0000000..6170a8d --- /dev/null +++ b/src/Text/Def/Internal/TextUI/TextPoint.cs @@ -0,0 +1,357 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.ObjectModel; +using Microsoft.VisualStudio.Text.Operations; + +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Represents a point in the <see cref="TextBuffer"/>. + /// </summary> + /// <remarks> + /// <para> + /// While this point is immutable, its position may change in response + /// to edits in the text. + /// </para> + /// </remarks> + public abstract class TextPoint + { + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextBuffer"/> of this point. + /// </summary> + public abstract TextBuffer TextBuffer { get; } + + /// <summary> + /// When implemented in a derived class, gets the integer representation of the current position of this text point + /// in relation to the start of the buffer. + /// </summary> + public abstract int CurrentPosition { get; } + + /// <summary> + /// When implemented in a derived class, gets the integer representation of the current position of this text point + /// in relation to the start of the line this point is on. + /// </summary> + public abstract int Column { get; } + + /// <summary> + /// When implemented in a derived class, deletes the character after this text point. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool DeleteNext(); + + /// <summary> + /// When implemented in a derived class, deletes the character before this text point. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool DeletePrevious(); + + /// <summary> + /// When implemented in a derived class, gets a text point for the first non-whitespace + /// character on the current line. + /// </summary> + /// <remarks> + /// <para> + /// If a line is all white space, this method returns a <see cref="TextPoint"/> at the end of the line, but before the + /// line break. + /// </para> + /// </remarks> + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)] + public abstract TextPoint GetFirstNonWhiteSpaceCharacterOnLine(); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextRange"/> of the current word. The current word may be white space only. + /// </summary> + /// <returns>The <see cref="TextRange"/> of the current word.</returns> + public abstract TextRange GetCurrentWord(); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextRange"/> of the next word that is not white space. + /// </summary> + /// <returns>The <see cref="TextRange"/> of the next word.</returns> + public abstract TextRange GetNextWord(); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextRange"/> of the previous word that is not white space. + /// </summary> + /// <returns>The <see cref="TextRange"/> of the previous word.</returns> + public abstract TextRange GetPreviousWord(); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextRange"/> that has this point and <paramref name="otherPoint"/> + /// as its start and end points. + /// </summary> + /// <returns>The <see cref="TextRange"/> that starts at this point and ends at <paramref name="otherPoint"/>.</returns> + /// <exception cref="InvalidOperationException"><paramref name="otherPoint"/> does not belong to the same buffer as this point.</exception> + public abstract TextRange GetTextRange(TextPoint otherPoint); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextRange"/> that has this point and <paramref name="otherPosition"/> + /// as its start and end positions. + /// </summary> + /// <returns>The <see cref="TextRange"/> that starts at this point and ends at <paramref name="otherPosition"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="otherPosition"/> is negative or past the end of this buffer.</exception> + public abstract TextRange GetTextRange(int otherPosition); + + /// <summary> + /// When implemented in a derived class, inserts a new line character at this text point. + /// </summary> + /// <returns> + /// Whether the edit succeeded. + /// </returns> + public abstract bool InsertNewLine(); + + /// <summary> + /// When implemented in a derived class, inserts a logical tab at this text point. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool InsertIndent(); + + /// <summary> + /// When implemented in a derived class, inserts <paramref name="text"/> at this text point. + /// </summary> + /// <param name="text">The text to insert.</param> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool InsertText(string text); + + /// <summary> + /// When implemented in a derived class, gets the line this text point is on. + /// </summary> + public abstract int LineNumber { get; } + + /// <summary> + /// When implemented in a derived class, gets the position of the start of the line this text point is on. + /// </summary> + public abstract int StartOfLine { get; } + + /// <summary> + /// When implemented in a derived class, gets the position of the end of the line this text point is on. + /// </summary> + public abstract int EndOfLine { get; } + + /// <summary> + /// When implemented in a derived class, removes a logical tab before this text point. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool RemovePreviousIndent(); + + /// <summary> + /// When implemented in a derived class, transposes the two characters on either side of this text point. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool TransposeCharacter(); + + /// <summary> + /// When implemented in a derived class, transposes the line this point is one with the next line. If this point is on the last + /// line of the file, the line is transposed with the previous one. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool TransposeLine(); + + /// <summary> + /// When implemented in a derived class, transposes the line this point is one with the given line number. + /// </summary> + /// <param name="lineNumber">The line number with which to transpose the line this point is on.</param> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool TransposeLine(int lineNumber); + + /// <summary> + /// When implemented in a derived class, gets the underlying <see cref="SnapshotPoint"/> of this text point. + /// </summary> + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)] + public abstract SnapshotPoint AdvancedTextPoint + { + get; + } + + /// <summary> + /// When implemented in a derived class, gets the next character after this text point. + /// </summary> + public abstract string GetNextCharacter(); + + /// <summary> + /// When implemented in a derived class, gets the previous character before this text point. + /// </summary> + /// <returns>The previous character.</returns> + public abstract string GetPreviousCharacter(); + + /// <summary> + /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> between this text point and <paramref name="endPoint"/>. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <param name="findOptions">The options to use while searching.</param> + /// <param name="endPoint">The text point at which to stop searching.</param> + /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at this text point.</returns> + public abstract TextRange Find(string pattern, FindOptions findOptions, TextPoint endPoint); + + /// <summary> + /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> between this text point and <paramref name="endPoint"/>. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <param name="endPoint">The text point at which to stop searching.</param> + /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at this text point.</returns> + public abstract TextRange Find(string pattern, TextPoint endPoint); + + /// <summary> + /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> starting from this text point. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <param name="findOptions">The options to use while searching.</param> + /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at this text point.</returns> + public abstract TextRange Find(string pattern, FindOptions findOptions); + + /// <summary> + /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> starting from this text point. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at this text point.</returns> + public abstract TextRange Find(string pattern); + + /// <summary> + /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> between this text point and <paramref name="endPoint"/>. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <param name="endPoint">The text point at which to stop searching.</param> + /// <returns>A list of matches in the order they were found.</returns> + public abstract Collection<TextRange> FindAll(string pattern, TextPoint endPoint); + + /// <summary> + /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> between this text point and <paramref name="endPoint"/>. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <param name="endPoint">The text point at which to stop searching.</param> + /// <param name="findOptions">The options to use while searching.</param> + /// <returns>A list of matches in the order they were found.</returns> + public abstract Collection<TextRange> FindAll(string pattern, FindOptions findOptions, TextPoint endPoint); + + /// <summary> + /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> starting from this text point. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <returns>A list of matches in the order they were found.</returns> + public abstract Collection<TextRange> FindAll(string pattern); + + /// <summary> + /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> starting from this text point. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <param name="findOptions">The options to use while searching.</param> + /// <returns>A list of matches in the order they were found.</returns> + public abstract Collection<TextRange> FindAll(string pattern, FindOptions findOptions); + + /// <summary> + /// When implemented in a derived class, moves this text point to a specific location. + /// </summary> + /// <param name="position">The new position of this text point.</param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is negative or past the end of this buffer.</exception> + public abstract void MoveTo(int position); + + /// <summary> + /// When implemented in a derived class, moves this point to the next character in the buffer. + /// </summary> + public abstract void MoveToNextCharacter(); + + /// <summary> + /// When implemented in a derived class, moves this point to the previous character in the buffer. + /// </summary> + /// + public abstract void MoveToPreviousCharacter(); + + /// <summary> + /// Creates a new <see cref="TextPoint"/> at this position that can be + /// moved independently from this one. + /// </summary> + /// <returns>The cloned <see cref="TextPoint"/>.</returns> + public TextPoint Clone() + { + return CloneInternal(); + } + + /// <summary> + /// When implemented in a derived class, clones the <see cref="TextPoint"/>. + /// </summary> + /// <returns>The cloned <see cref="TextPoint"/>.</returns> + protected abstract TextPoint CloneInternal(); + + /// <summary> + /// When implemented in a derived class, moves the text point at the start of the specified line and ensures it is visible. + /// </summary> + /// <param name="lineNumber"> + /// The line number on which to position the text point. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than zero + /// or greater than the line number of the last line in the text buffer.</exception> + public abstract void MoveToLine(int lineNumber); + + /// <summary> + /// When implemented in a derived class, movea this point to the end of the line that it is currently on. + /// </summary> + public abstract void MoveToEndOfLine(); + + /// <summary> + /// When implemented in a derived class, moves this point to the start of the line that it is currently on. + /// </summary> + public abstract void MoveToStartOfLine(); + + /// <summary> + /// When implemented in a derived class, moves this point to the end of the document. + /// </summary> + public abstract void MoveToEndOfDocument(); + + /// <summary> + /// When implemented in a derived class, moves this point to the start of the document. + /// </summary> + public abstract void MoveToStartOfDocument(); + + /// <summary> + /// When implemented in a derived class, moves this point to the beginning of the next line. + /// </summary> + /// <remarks> + /// This point moves to the end of the line if the point is on the last + /// line. + /// </remarks> + public abstract void MoveToBeginningOfNextLine(); + + /// <summary> + /// When implemented in a derived class, moves this point to the beginning of the previous line. + /// </summary> + /// <remarks> + /// This point moves to the start of the line if the point is on the first + /// line. + /// </remarks> + public abstract void MoveToBeginningOfPreviousLine(); + + /// <summary> + /// When implemented in a derived class, moves this point to the end of the current word, or to the beginning of the + /// next word if the point is already at the end of the current word. + /// </summary> + public abstract void MoveToNextWord(); + + /// <summary> + /// When implemented in a derived class, moves this point to the start of the current word, or the end of the + /// previous word if the point is already at the start of the current word. + /// </summary> + public abstract void MoveToPreviousWord(); + } +} diff --git a/src/Text/Def/Internal/TextUI/TextRange.cs b/src/Text/Def/Internal/TextUI/TextRange.cs new file mode 100644 index 0000000..3b114ad --- /dev/null +++ b/src/Text/Def/Internal/TextUI/TextRange.cs @@ -0,0 +1,257 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Collections; +using System.Collections.ObjectModel; +using Microsoft.VisualStudio.Text.Operations; + +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Represents a range of text in the buffer. + /// </summary> + /// <remarks> + /// <para> + /// While this range is immutable, edits to the text will cause + /// it to adjust its location in response to the edits. + /// </para> + /// </remarks> + public abstract class TextRange : IEnumerable<TextPoint> + { + /// <summary> + /// When implemented in a derived class, gets the start point of this text range. + /// </summary> + /// <returns>The starting <see cref="TextPoint"/>.</returns> + public abstract TextPoint GetStartPoint(); + + /// <summary> + /// When implemented in a derived class, gets the end point of this text range. + /// </summary> + /// <returns>The end <see cref="TextPoint"/>.</returns> + public abstract TextPoint GetEndPoint(); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextBuffer"/> of this text range. + /// </summary> + /// <returns>The <see cref="TextBuffer"/> of this text range.</returns> + public abstract TextBuffer TextBuffer + { + get; + } + + /// <summary> + /// When implemented in a derived class, gets the underlying <see cref="SnapshotSpan"/> of this <see cref="TextRange"/>. + /// The <see cref="SnapshotSpan"/> should be used only for advanced functionality. + /// </summary> + /// <returns>The <see cref="SnapshotSpan"/>.</returns> + [EditorBrowsable(EditorBrowsableState.Advanced)] + public abstract SnapshotSpan AdvancedTextRange + { + get; + } + + /// <summary> + /// When implemented in a derived class, makes the text in this range uppercase. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + /// <remarks><para>If the range is empty, will apply to the character next to the range only.</para></remarks> + public abstract bool MakeUppercase(); + + /// <summary> + /// When implemented in a derived class, makes the text in the this range lowercase. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + /// <remarks><para>If the range is empty, this method applies to the character next to the range only.</para></remarks> + public abstract bool MakeLowercase(); + + /// <summary> + /// When implemented in a derived class, makes the first character in every word in this range uppercase, and makes the rest of the characters lowercase. + /// </summary> + /// <remarks> + /// If the range is empty, this method applies to the character next to the range only. + /// If the range starts in the middle of a word, only the part in the range will be made lowercase. + /// </remarks> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool Capitalize(); + + /// <summary> + /// When implemented in a derived class, switches the case of every character in this range. + /// </summary> + /// <remarks><para>If the range is empty, this method applies to the character next to the range only.</para></remarks> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool ToggleCase(); + + /// <summary> + /// When implemented in a derived class, deletes all the text in this range. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool Delete(); + + /// <summary> + /// When implemented in a derived class, indents all the lines in this range. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool Indent(); + + /// <summary> + /// When implemented in a derived class, unindents all the lines in this range. + /// </summary> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool Unindent(); + + /// <summary> + /// When implemented in a derived class, determines whether the <see cref="TextRange"/> is zero-length. + /// </summary> + /// <returns><c>true</c> if the <see cref="TextRange"/> is zero length, <c>false</c> otherwise.</returns> + public abstract bool IsEmpty + { + get; + } + + /// <summary> + /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> in this text range. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at the start point of this range.</returns> + public abstract TextRange Find(string pattern); + + /// <summary> + /// When implemented in a derived class, finds the start of the first occurrence of <paramref name="pattern"/> in this text range. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <param name="findOptions">The options to use while searching.</param> + /// <returns>The text range of the first occurrence of the pattern if it was found, otherwise a zero-length text range at the start point of this range.</returns> + public abstract TextRange Find(string pattern, FindOptions findOptions); + + /// <summary> + /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> starting in this text range. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <returns>A list of matches in the order they were found.</returns> + public abstract Collection<TextRange> FindAll(string pattern); + + /// <summary> + /// When implemented in a derived class, finds all matches of <paramref name="pattern"/> starting in this text range. + /// </summary> + /// <param name="pattern">The pattern to find.</param> + /// <param name="findOptions">The options to use while searching.</param> + /// <returns>A list of matches in the order they were found.</returns> + public abstract Collection<TextRange> FindAll(string pattern, FindOptions findOptions); + + /// <summary> + /// When implemented in a derived class, replaces the text in this range with <paramref name="newText"/>. + /// </summary> + /// <param name="newText">The new text.</param> + /// <remarks> + /// This <see cref="TextRange"/> spans the new text after it has been replaced. + /// </remarks> + /// <returns> + /// <c>true</c> if the edit succeeded, otherwise <c>false</c>. + /// </returns> + public abstract bool ReplaceText(string newText); + + /// <summary> + /// When implemented in a derived class, gets the text in this range. + /// </summary> + /// <returns>The text in the range.</returns> + public abstract string GetText(); + + /// <summary> + /// When implemented in a derived class, creates a clone of this text range than can be moved independently of this one. + /// </summary> + /// <returns>The cloned <see cref="TextRange"/>.</returns> + public TextRange Clone() + { + return CloneInternal(); + } + + /// <summary> + /// When implemented in a derived class, clones the text range. + /// </summary> + /// <returns>The cloned <see cref="TextRange"/>.</returns> + protected abstract TextRange CloneInternal(); + + /// <summary> + /// When implemented in a derived class, sets the start point of this text range. + /// </summary> + /// <param name="startPoint">The new start point.</param> + /// <remarks> + /// If <paramref name="startPoint"/> occurs after + /// the current end point in the buffer, <paramref name="startPoint"/> becomes the end point, and the current end point becomes the start point. + /// </remarks> + /// <exception cref="InvalidOperationException"><paramref name="startPoint"/> belongs to another buffer.</exception> + public abstract void SetStart(TextPoint startPoint); + + /// <summary> + /// When implemented in a derived class, sets the end point of this text range. + /// </summary> + /// <param name="endPoint">The new end point.</param> + /// <remarks> + /// <para> + /// If <paramref name="endPoint"/> is before + /// the current start point in the buffer, <paramref name="endPoint"/> becomes the start point, and the current start point becomes the end point. + /// </para> + /// </remarks> + /// <exception cref="InvalidOperationException"><paramref name="endPoint"/> belongs to another buffer.</exception> + public abstract void SetEnd(TextPoint endPoint); + + /// <summary> + /// When implemented in a derived class, moves this text range to the range of <paramref name="newRange"/>. + /// </summary> + /// <param name="newRange">The new range.</param> + public abstract void MoveTo(TextRange newRange); + + /// <summary> + /// When implemented in a derived class, gets the enumerator of type <see cref="TextPoint"/>. + /// </summary> + /// <returns>the enumerator of type <see cref="TextPoint"/>.</returns> + protected abstract IEnumerator<TextPoint> GetEnumeratorInternal(); + + #region IEnumerable<TextPoint> Members + + /// <summary> + /// Gets the enumerator. + /// </summary> + /// <returns>An enumerator of type <see cref="TextPoint"/>.</returns> + public IEnumerator<TextPoint> GetEnumerator() + { + return GetEnumeratorInternal(); + } + + #endregion + + #region IEnumerable Members + + /// <summary> + /// Gets the enumerator. + /// </summary> + /// <returns>The enumerator.</returns> + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/src/Text/Def/Internal/TextUI/TextView.cs b/src/Text/Def/Internal/TextUI/TextView.cs new file mode 100644 index 0000000..e1a3fa6 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/TextView.cs @@ -0,0 +1,165 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + using System; + using Microsoft.VisualStudio.Text.Formatting; + + /// <summary> + /// Provides methods for scrolling the editor window up and down. + /// </summary> + public abstract class TextView + { + /// <summary> + /// When implemented in a derived class, moves the current line to the top of the view without moving the caret. + /// </summary> + /// <param name="lineNumber">The number of lines to move.</param> + public abstract void MoveLineToTop(int lineNumber); + + /// <summary> + /// When implemented in a derived class, moves the current line to the bottom of the view without moving the caret. + /// </summary> + /// <param name="lineNumber">The number of lines to move.</param> + public abstract void MoveLineToBottom(int lineNumber); + + /// <summary> + /// When implemented in a derived class, scrolls the view up by one line. + /// </summary> + /// <param name="lines">The number of lines to scroll.</param> + public abstract void ScrollUp(int lines); + + /// <summary> + /// When implemented in a derived class, scrolls the view down by one line. + /// </summary> + /// <param name="lines">The number of lines to scroll.</param> + public abstract void ScrollDown(int lines); + + /// <summary> + /// When implemented in a derived class, scrolls the view down by one page and does not move the caret. + /// </summary> + public abstract void ScrollPageDown(); + + /// <summary> + /// When implemented in a derived class, scrolls the view up by one page and does not move the caret. + /// </summary> + public abstract void ScrollPageUp(); + + /// <summary> + /// When implemented in a derived class, shows the <paramref name="point"/> in the view. + /// </summary> + /// <param name="point">The point to display.</param> + /// <param name="howToShow">How the point should be displayed on the screen.</param> + /// <returns><c>true</c> if the point was actually displayed, otherwise <c>false</c>.</returns> + public abstract bool Show(DisplayTextPoint point, HowToShow howToShow); + + /// <summary> + /// When implemented in a derived class, shows the <paramref name="textRange"/> in the view. + /// </summary> + /// <param name="textRange">The <see cref="TextRange"/> to display.</param> + /// <param name="howToShow">How the point should be displayed on the screen.</param> + /// <returns>The <see cref="VisibilityState"/> that describes how the range was actually displayed.</returns> + public abstract VisibilityState Show(DisplayTextRange textRange, HowToShow howToShow); + + /// <summary> + /// When implemented in a derived class, gets a display text point from a integer position. + /// </summary> + /// <param name="position">The position at which to get the text point.</param> + /// <returns>The <see cref="DisplayTextPoint"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is negative or past the end of the buffer.</exception> + public abstract DisplayTextPoint GetTextPoint(int position); + + /// <summary> + /// When implemented in a derived class, gets a display text point from a buffer text point position. + /// </summary> + /// <param name="textPoint">The buffer text point to translate into a display text point.</param> + /// <returns>The <see cref="DisplayTextPoint"/>.</returns> + /// <exception cref="ArgumentException"><paramref name="textPoint"/> does not belong to the same buffer as the view.</exception> + public abstract DisplayTextPoint GetTextPoint(TextPoint textPoint); + + /// <summary> + /// When implemented in a derived class, gets a display text point from a line and column. + /// </summary> + /// <param name="line">The line on which to get this text point.</param> + /// <param name="column">The line-relative position at which to get the text point.</param> + /// <returns>The <see cref="DisplayTextPoint"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="line"/> is negative or greater than the number of lines in the buffer, or + /// <paramref name="column"/> is negative or past the end of the line.</exception> + public abstract DisplayTextPoint GetTextPoint(int line, int column); + + /// <summary> + /// When implemented in a derived class, gets a display text range from two display text points. + /// </summary> + /// <param name="startPoint">The start point of the range.</param> + /// <param name="endPoint">The end point of the range.</param> + /// <returns>The text range that starts and ends at the two points.</returns> + /// <remarks>The start point of the text range may become the end point if the start point occurs after the end point.</remarks> + /// <exception cref="ArgumentException"><paramref name="startPoint"/> or <paramref name="endPoint"/> do not belong to this buffer.</exception> + public abstract DisplayTextRange GetTextRange(TextPoint startPoint, TextPoint endPoint); + + /// <summary> + /// When implemented in a derived class, gets a display text range from a text range on the buffer. + /// </summary> + /// <param name="textRange">The text range on the buffer.</param> + /// <returns>The <see cref="DisplayTextPoint"/>.</returns> + /// <remarks>The start point of the text range may become the end point if the start point occurs after the end point.</remarks> + /// <exception cref="ArgumentException"><paramref name="textRange"/> does not belong to the same buffer as the view.</exception> + public abstract DisplayTextRange GetTextRange(TextRange textRange); + + /// <summary> + /// When implemented in a derived class, gets a display text range from two integer positions. + /// </summary> + /// <param name="startPosition">The start position of the range.</param> + /// <param name="endPosition">The end position of the range.</param> + /// <returns>The text range that starts and ends at the two positions.</returns> + /// <remarks>The start position of the text range may become the end point if the start position occurs after the end position.</remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="startPosition"/> is negative or past the end of the buffer.</exception> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="endPosition"/> is negative or past the end of the buffer.</exception> + public abstract DisplayTextRange GetTextRange(int startPosition, int endPosition); + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextRange"/> of text currently visible on screen. + /// </summary> + public abstract DisplayTextRange VisibleSpan + { + get; + } + + /// <summary> + /// When implemented in a derived class, provides advanced view manipulation functionality. + /// </summary> + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Advanced)] + public abstract ITextView AdvancedTextView + { + get; + } + + /// <summary> + /// When implemented in a derived class, gets the <see cref="Caret"/> of this view. + /// </summary> + public abstract Caret Caret + { + get; + } + + /// <summary> + /// When implemented in a derived class, gets the <see cref="Selection"/>. of this view. + /// </summary> + public abstract Selection Selection + { + get; + } + + /// <summary> + /// When implemented in a derived class, gets the <see cref="TextBuffer"/>. of this view. + /// </summary> + public abstract TextBuffer TextBuffer + { + get; + } + } +} diff --git a/src/Text/Def/Internal/TextUI/TrackChangesFormatDefinitions.cs b/src/Text/Def/Internal/TextUI/TrackChangesFormatDefinitions.cs new file mode 100644 index 0000000..0217538 --- /dev/null +++ b/src/Text/Def/Internal/TextUI/TrackChangesFormatDefinitions.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + public static class TrackChangesFormatDefinitions + { + public const string TrackChangesBeforeSave = "Track Changes before save"; + public const string TrackChangesAfterSave = "Track Changes after save"; + public const string TrackChangesReverted = "Track reverted changes"; + } +} diff --git a/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs b/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs new file mode 100644 index 0000000..adbb0ca --- /dev/null +++ b/src/Text/Def/Internal/TextUI/ViewRelativePosition2.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Defines the meaning of the verticalOffset parameter in the <see cref="ITextView"/>.DisplayTextLineContaining(...). + /// </summary> + /// <remarks> + /// This enum adds a couple of modes to the ViewRelativePosition that we need to support for the InterLineAdornments + /// but don't want to expose. The WpfTextView accepts the new values but doesn't pass them on to anyone else. + /// </remarks> + public enum ViewRelativePosition2 + { + /// <summary> + /// The offset with respect to the top of the view to the top of the line. + /// </summary> + /// <remarks> + /// Must match ViewRelativePosition.Top. + /// </remarks> + Top = ViewRelativePosition.Top, + + /// <summary> + /// The offset with respect to the bottom of the view to the bottom of the line. + /// </summary> + /// <remarks> + /// Must match ViewRelativePosition.Bottom. + /// </remarks> + Bottom = ViewRelativePosition.Bottom, + + /// <summary> + /// The offset with respect to the top of the view to the top of the text on the line. + /// </summary> + TextTop, + + /// <summary> + /// The offset with respect to the bottom of the view to the bottom of the text on the line. + /// </summary> + TextBottom + } +}
\ No newline at end of file diff --git a/src/Text/Def/TextData/Model/ITextBuffer2.cs b/src/Text/Def/TextData/Model/ITextBuffer2.cs new file mode 100644 index 0000000..a755074 --- /dev/null +++ b/src/Text/Def/TextData/Model/ITextBuffer2.cs @@ -0,0 +1,35 @@ +// +// 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 +{ + using System; + + /// <summary> + /// A mutable sequence of Unicode characters encoded using UTF-16. + /// Positions within the buffer are treated as a sequence of characters (starting at character zero) or + /// as a sequence of lines (starting at line zero). An empty buffer has a single line containing no characters. + /// </summary> + /// <remarks>Any <see cref="ITextBuffer"/> will be upcastable to an <see cref="ITextBuffer2"/>.</remarks> + public interface ITextBuffer2 : ITextBuffer + { + /// <summary> + /// Occurs when a non-empty <see cref="ITextEdit"/> is successfully applied. + /// This is raised on a background thread. Listeners are expected to schedule any expensive + /// work to be done asynchronously outside of this thread. + /// </summary> + /// <remarks> + /// Listeners of this event are not expected to modify the buffer. For performance reasons + /// handlers that modify the buffer should listen to <see cref="ITextBuffer.ChangedHighPriority"/> event + /// instead. + /// <para> + /// This event is raised after <see cref="ITextBuffer.ChangedHighPriority"/> event. + /// It's guaranteed that individual listeners receive the <see cref="ChangedOnBackground"/> events + /// in a synchronized way (never on more than one thread at a time) and in the order + /// the edits were applied. + /// </para> + /// </remarks> + event EventHandler<TextContentChangedEventArgs> ChangedOnBackground; + } +} diff --git a/src/Text/Def/TextData/Model/ITextBufferFactoryService3.cs b/src/Text/Def/TextData/Model/ITextBufferFactoryService3.cs new file mode 100644 index 0000000..cbd5830 --- /dev/null +++ b/src/Text/Def/TextData/Model/ITextBufferFactoryService3.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain internal APIs that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text +{ + using System; + using System.IO; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// The factory service for ordinary TextBuffers. + /// </summary> + /// <remarks><para>This is a MEF Component, and should be imported as follows: + /// <code> + /// [Import] + /// ITextBufferFactoryService factory = null; + /// </code> + /// </para> + /// <para>Any <see cref="ITextBufferFactoryService"/> will be upcastable to an <see cref="ITextBufferFactoryService3"/>.</para> + /// </remarks> + public interface ITextBufferFactoryService3 : ITextBufferFactoryService // Annoying: we need to maintain ITextBufferFactoryService2 for Roslyn, but it is internal (and has to stay there). + { + /// <summary> + /// Creates an <see cref="ITextBuffer"/> with the specified <see cref="IContentType"/> and populates it + /// with the given text contained in <paramref name="span"/>. + /// </summary> + /// <param name="span">The initial text to add.</param> + /// <param name="contentType">The <see cref="IContentType"/> for the new <see cref="ITextBuffer"/>.</param> + /// <returns> + /// A <see cref="ITextBuffer"/> object with the given text and <see cref="IContentType"/>. + /// </returns> + /// <exception cref="ArgumentNullException">Either <paramref name="span"/> or <paramref name="contentType"/> is null.</exception> + ITextBuffer CreateTextBuffer(SnapshotSpan span, IContentType contentType); + + /// <summary> + /// Creates an <see cref="ITextBuffer"/> with the given <paramref name="contentType"/> and populates it by + /// reading data from the specified TextReader. + /// </summary> + /// <param name="reader">The TextReader from which to read.</param> + /// <param name="contentType">The <paramref name="contentType"/> for the text contained in the new <see cref="ITextBuffer"/></param> + /// <param name="length">The length of the file backing the text reader, if known; otherwise -1.</param> + /// <param name="traceId">An optional identifier used in debug tracing.</param> + /// <returns> + /// An <see cref="ITextBuffer"/> object with the given TextReader and <paramref name="contentType"/>. + /// </returns> + /// <exception cref="ArgumentNullException"><paramref name="reader"/> is null.</exception> + /// <exception cref="ArgumentNullException"><paramref name="contentType"/> is null.</exception> + /// <remarks> + /// <para>The <paramref name="reader"/> is not closed by this operation.</para> + /// <para>The <paramref name="length"/> is used to help select a storage strategy for the text buffer.</para> + /// </remarks> + ITextBuffer CreateTextBuffer(TextReader reader, IContentType contentType, long length = -1, string traceId = ""); + + /// <summary> + /// Creates an <see cref="ITextBuffer"/> with the specified <see cref="IContentType"/> and populates it + /// with the text contained in <paramref name="image"/>. + /// </summary> + /// <param name="image">The initial text of the buffer.</param> + /// <param name="contentType">The <see cref="IContentType"/> for the new <see cref="ITextBuffer"/>.</param> + /// <returns> + /// A <see cref="ITextBuffer"/> object with the given text and <see cref="IContentType"/>. + /// </returns> + /// <remarks>The new <see cref="ITextBuffer"/> will not inherit the version history of <paramref name="image"/>.</remarks> + /// <exception cref="ArgumentNullException">Either <paramref name="image"/> or <paramref name="contentType"/> is null.</exception> + ITextBuffer CreateTextBuffer(ITextImage image, IContentType contentType); + } +} diff --git a/src/Text/Def/TextData/Model/ITextImage.cs b/src/Text/Def/TextData/Model/ITextImage.cs new file mode 100644 index 0000000..59379d6 --- /dev/null +++ b/src/Text/Def/TextData/Model/ITextImage.cs @@ -0,0 +1,174 @@ +// +// 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 +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Provides read access to an immutable sequence of Unicode characters. + /// The first character in the sequence has index zero. + /// </summary> + public interface ITextImage + { + /// <summary> + /// The <see cref="ITextImageVersion"/> associated with this <see cref="ITextImage"/>. + /// </summary> + /// <remarks> + /// This will be null unless this <see cref="ITextImage"/> was created by a component that + /// manages the version information.</remarks> + ITextImageVersion Version { get; } + + /// <summary> + /// Create a new <see cref="ITextImage"/> that is a clone of a subspan of this <see cref="ITextImage"/>. + /// </summary> + /// <remarks> + /// <para>The new <see cref="ITextImage"/> will not inherit the version or version history of this <see cref="ITextImage"/>.</para> + /// <para>The <see cref="ITextImage.Version"/> of the returned image will be null even the contents are identical to this.</para>/// </remarks> + ITextImage GetSubText(Span span); + + /// <summary> + /// Gets the number of UTF-16 characters contained in the image. + /// </summary> + int Length { get; } + + /// <summary> + /// Gets the positive number of lines in the image. An image whose <see cref="Length"/> is zero is considered to have one line. + /// </summary> + int LineCount { get; } + + /// <summary> + /// Gets text from the image starting at the beginning of the span and having length equal to the length of the span. + /// </summary> + /// <param name="span">The span to return.</param> + /// <exception cref="ArgumentOutOfRangeException">The end of the span is greater than <see cref="Length"/>.</exception> + /// <returns>A non-null string.</returns> + string GetText(Span span); + + /// <summary> + /// Converts a range of text to a character array. + /// </summary> + /// <param name="startIndex"> + /// The starting index of the range of text. + /// </param> + /// <param name="length"> + /// The length of the text. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex"/> is less than zero or greater than the length of the image, or + /// <paramref name="length"/> is less than zero, or <paramref name="startIndex"/> plus <paramref name="length"/> is greater than the length of the image.</exception> + /// <returns>The array of characters starting at <paramref name="startIndex"/> in the underlying <see cref="ITextBuffer"/> and extend to its end.</returns> + char[] ToCharArray(int startIndex, int length); + + /// <summary> + /// Copies a range of text to a character array. + /// </summary> + /// <param name="sourceIndex"> + /// The starting index in the text image. + /// </param> + /// <param name="destination"> + /// The destination array. + /// </param> + /// <param name="destinationIndex"> + /// The index in the destination array at which to start copying the text. + /// </param> + /// <param name="count"> + /// The number of characters to copy. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="sourceIndex"/> is less than zero or greater than the length of the image, or + /// <paramref name="count"/> is less than zero, or <paramref name="sourceIndex"/> + <paramref name="count"/> is greater than the length of the image, or + /// <paramref name="destinationIndex"/> is less than zero, or <paramref name="destinationIndex"/> plus <paramref name="count"/> is greater than the length of <paramref name="destination"/>.</exception> + /// <exception cref="ArgumentNullException"><paramref name="destination"/> is null.</exception> + void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count); + + /// <summary> + /// Gets a single character at the specified position. + /// </summary> + /// <param name="position">The position of the character.</param> + /// <returns>The character at <paramref name="position"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than or equal to the length of the image.</exception> + char this[int position] { get; } + + /// <summary> + /// Gets an <see cref="TextImageLine"/> for the given line number. + /// </summary> + /// <param name="lineNumber">The line number.</param> + /// <returns>A non-null <see cref="TextImageLine"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than zero or greater than or equal to <see cref="LineCount"/>.</exception> + TextImageLine GetLineFromLineNumber(int lineNumber); + + /// <summary> + /// Gets an <see cref="TextImageLine"/> for a line at the given position. + /// </summary> + /// <param name="position">The position.</param> + /// <returns>A non-null <see cref="TextImageLine"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than length of line.</exception> + TextImageLine GetLineFromPosition(int position); + + /// <summary> + /// Gets the number of the line that contains the character at the specified position. + /// </summary> + /// <returns>The line number of the line in which <paramref name="position"/> lies.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than Length/>.</exception> + int GetLineNumberFromPosition(int position); + + /// <summary> + /// Writes a substring of the contents of the image. + /// </summary> + /// <param name="writer">The <see cref="System.IO.TextWriter"/> to use.</param> + /// <param name="span">The span of text to write.</param> + /// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception> + /// <exception cref="ArgumentOutOfRangeException">The end of the span is greater than the length of the image. + /// </exception> + void Write(System.IO.TextWriter writer, Span span); + } + + public static class TextImageExtensions + { + /// <summary> + /// Gets text from the image starting at <paramref name="startIndex"/> and having length equal to <paramref name="length"/>. + /// </summary> + /// <param name="startIndex">The starting index.</param> + /// <param name="length">The length of text to get.</param> + /// <returns>The string of length <paramref name="length"/> starting at <paramref name="startIndex"/> in the underlying <see cref="ITextBuffer"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex"/> is less than zero or greater than the length of the image, + /// or <paramref name="length"/> is less than zero, or <paramref name="startIndex"/> plus <paramref name="length"/> is greater than the length of the image.</exception> + public static string GetText(this ITextImage image, int startIndex, int length) + { + return image.GetText(new Span(startIndex, length)); + } + + /// <summary> + /// Gets all the text in the image. + /// </summary> + /// <returns>A non-null string.</returns> + /// <remarks>Caveat emptor. Calling GetText() on a 100MB <see cref="ITextImage"/> will give you exactly what you asked for, which + /// probably isn't what you wanted.</remarks> + public static string GetText(this ITextImage image) + { + return image.GetText(new Span(0, image.Length)); + } + + /// <summary> + /// Create a new <see cref="ITextImage"/> that is a clone of a subspan of this <see cref="ITextImage"/>. + /// </summary> + /// <remarks> + /// The new <see cref="ITextImage"/> will not inherit the version or version history of this <see cref="ITextImage"/>.</remarks> + public static ITextImage GetSubText(this ITextImage image, int startIndex, int length) + { + return image.GetSubText(new Span(startIndex, length)); + } + + /// <summary> + /// Writes the contents of the image. + /// </summary> + /// <param name="writer">The <see cref="System.IO.TextWriter"/>to use.</param> + /// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception> + public static void Write(this ITextImage image, System.IO.TextWriter writer) + { + image.Write(writer, new Span(0, image.Length)); + } + } +} diff --git a/src/Text/Def/TextData/Model/ITextImageFactoryService.cs b/src/Text/Def/TextData/Model/ITextImageFactoryService.cs new file mode 100644 index 0000000..797f7b1 --- /dev/null +++ b/src/Text/Def/TextData/Model/ITextImageFactoryService.cs @@ -0,0 +1,35 @@ +// +// 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 +{ + using System; + using System.IO; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// The factory service for creating <see cref="ITextImage"/>s. + /// </summary> + /// <remarks>This is a MEF Component, and should be imported as follows: + /// <code> + /// [Import] + /// ITextImageFactoryService factory = null; + /// </code> + /// </remarks> + public interface ITextImageFactoryService + { + /// <summary> + /// Create an <see cref="ITextImage"/> that contains <paramref name="text"/>. + /// </summary> + ITextImage CreateTextImage(string text); + + /// <summary> + /// Create an <see cref="ITextImage"/> that contains the contents of <paramref name="reader"/>. + /// </summary> + /// <param name="length">An estimate of the total length of the file. -1 if the file size is unknown.</param> + /// <remarks> + /// <para>The <paramref name="length"/> is used to decide whether or not to attempt to compress the text read from <paramref name="reader"/>. It does not need to be accurate.</para></remarks> + ITextImage CreateTextImage(TextReader reader, long length = -1); + } +} diff --git a/src/Text/Def/TextData/Model/ITextImageVersion.cs b/src/Text/Def/TextData/Model/ITextImageVersion.cs new file mode 100644 index 0000000..bbf36e6 --- /dev/null +++ b/src/Text/Def/TextData/Model/ITextImageVersion.cs @@ -0,0 +1,70 @@ +// +// 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 +{ + using System; + + /// <summary> + /// Describes a version of an <see cref="ITextImage"/>. + /// </summary> + public interface ITextImageVersion + { + /// <summary> + /// Gets the next <see cref="ITextImageVersion"/>. Returns null if and only if this is the most recent version of its <see cref="ITextImage"/>. + /// </summary> + ITextImageVersion Next { get; } + + /// <summary> + /// Gets the length in characters of this <see cref="ITextImageVersion"/>. + /// </summary> + int Length { get; } + + /// <summary> + /// Gets the text changes that produce the next version. Returns null if and only if this is the most recent version of its <see cref="ITextImage"/>. + /// </summary> + INormalizedTextChangeCollection Changes { get; } + + /// <summary> + /// The version number for this version. + /// </summary> + /// <remarks>This starts at zero and is increased as new <see cref="ITextImage"/>s that are based on this <see cref="ITextImage"/> are created.</remarks> + int VersionNumber { get; } + + /// <summary> + /// Gets the oldest version number for which all text changes between that version and this version have + /// been canceled out by corresponding undo/redo operations. + /// </summary> + /// <remarks> + /// If ReiteratedVersionNumber is not equal to <see cref="VersionNumber" />, then for every + /// <see cref="ITextChange" /> not originated by an undo operation between ReiteratedVersionNumber and VersionNumber, there is a + /// corresponding <see cref="ITextChange"/> originated by an undo operation that cancels it out. So the contents of the two + /// versions are necessarily identical. + ///<para> + /// Setting this property correctly is the responsibility of the undo system; aside from this + /// property, the text buffer and related classes are unaware of undo and redo. + /// </para> + /// <para> + /// Note that the <see cref="ITextImageVersion"/> created through through an operation that makes no text changes + /// will therefore have the ReiteratedVersionNumber of the previous version. + /// </para> + /// </remarks> + int ReiteratedVersionNumber { get; } + + /// <summary> + /// A unique identifier associated with a version and all versions derived from it. + /// </summary> + object Identifier { get; } + + /// <summary> + /// Translate a position in another <see cref="ITextImageVersion"/> to this <see cref="ITextImageVersion"/>. + /// </summary> + int TrackTo(VersionedPosition other, PointTrackingMode mode); + + /// <summary> + /// Translate a span in another <see cref="ITextImageVersion"/> to this <see cref="ITextImageVersion"/>. + /// </summary> + Span TrackTo(VersionedSpan span, SpanTrackingMode mode); + } +} diff --git a/src/Text/Def/TextData/Model/ITextSnapshot2.cs b/src/Text/Def/TextData/Model/ITextSnapshot2.cs new file mode 100644 index 0000000..0979093 --- /dev/null +++ b/src/Text/Def/TextData/Model/ITextSnapshot2.cs @@ -0,0 +1,32 @@ +// +// 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 +{ + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Provides read access to an immutable snapshot of a <see cref="ITextBuffer"/> containing a sequence of Unicode characters. + /// The first character in the sequence has index zero. + /// </summary> + /// <remarks>Any <see cref="ITextSnapshot"/> will be upcastable to an <see cref="ITextSnapshot2"/>.</remarks> + public interface ITextSnapshot2 : ITextSnapshot + { + /// <summary> + /// Gets the underlying <see cref="ITextImage"/> of the snapshot. + /// </summary> + ITextImage TextImage { get; } + + /// <summary> + /// Save the entire snapshot to the specified <paramref name="filePath"/>. + /// </summary> + /// <param name="filePath">Path to save</param> + /// <param name="replaceFile">If true, replace an exising file.</param> + /// <param name="encoding">Encoding to use to save the file.</param> + void SaveToFile(string filePath, bool replaceFile, Encoding encoding); + } +} diff --git a/src/Text/Def/TextData/Model/ITextVersion2.cs b/src/Text/Def/TextData/Model/ITextVersion2.cs new file mode 100644 index 0000000..75dedc8 --- /dev/null +++ b/src/Text/Def/TextData/Model/ITextVersion2.cs @@ -0,0 +1,18 @@ +// +// 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 +{ + using System; + + /// <summary> + /// Describes a version of an <see cref="ITextBuffer"/>. Each application of an <see cref="ITextEdit"/> to a text buffer + /// generates a new <see cref="ITextVersion"/>. + /// </summary> + /// <remarks>Any <see cref="ITextVersion"/> will be upcastable to an <see cref="ITextVersion2"/>.</remarks> + public interface ITextVersion2 : ITextVersion + { + ITextImageVersion ImageVersion { get; } + } +} diff --git a/src/Text/Def/TextData/Model/TextImageLine.cs b/src/Text/Def/TextData/Model/TextImageLine.cs new file mode 100644 index 0000000..b7e95c1 --- /dev/null +++ b/src/Text/Def/TextData/Model/TextImageLine.cs @@ -0,0 +1,147 @@ +// +// 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 +{ + using System; + + /// <summary> + /// Immutable information about a line of text from an <see cref="ITextImage"/>. + /// </summary> + public struct TextImageLine : IEquatable<TextImageLine> + { + public readonly static TextImageLine Invalid = new TextImageLine(); + + public TextImageLine(ITextImage image, int lineNumber, Span extent, int lineBreakLength) + { + if (image == null) + throw new ArgumentNullException(nameof(image)); + + if ((lineNumber < 0) || (lineNumber >= image.LineCount)) + throw new ArgumentOutOfRangeException(nameof(lineNumber)); + + if (extent.End > image.Length) + throw new ArgumentOutOfRangeException(nameof(extent)); + + if ((lineBreakLength < 0) || (lineBreakLength > 2) || (extent.End + lineBreakLength > image.Length)) + throw new ArgumentOutOfRangeException(nameof(lineBreakLength)); + + this.Image = image; + this.LineNumber = lineNumber; + this.Extent = extent; + this.LineBreakLength = lineBreakLength; + } + + /// <summary> + /// The <see cref="ITextImage"/> in which the line appears. + /// </summary> + public readonly ITextImage Image; + + /// <summary> + /// The extent of the line, excluding any line break characters. + /// </summary> + public readonly Span Extent; + + /// <summary> + /// The extent of the line, including any line break characters. + /// </summary> + public Span ExtentIncludingLineBreak { get { return new Span(this.Extent.Start, this.LengthIncludingLineBreak); } } + + /// <summary> + /// The 0-origin line number of the line. + /// </summary> + public readonly int LineNumber; + + /// <summary> + /// The position of the first character in the line. + /// </summary> + public int Start { get { return this.Extent.Start; } } + + /// <summary> + /// Length of the line, excluding any line break characters. + /// </summary> + public int Length { get { return this.Extent.Length; } } + + /// <summary> + /// Length of the line, including any line break characters. + /// </summary> + public int LengthIncludingLineBreak { get { return this.Extent.Length + this.LineBreakLength; } } + + /// <summary> + /// The position of the first character past the end of the line, excluding any + /// line break characters (thus will address a line break character, except + /// for the last line in the buffer, in which case it addresses a + /// position past the end of the buffer). + /// </summary> + public int End { get { return this.Extent.End; } } + + /// <summary> + /// The position of the first character past the end of the line, including any + /// line break characters (thus will address the first character in + /// the succeeding line, unless this is the last line, in which case it addresses a + /// position past the end of the buffer). + /// </summary> + public int EndIncludingLineBreak { get { return this.Extent.End + this.LineBreakLength; } } + + /// <summary> + /// Length of line break characters (always falls in the range [0..2]). + /// </summary> + public readonly int LineBreakLength; + + /// <summary> + /// The text of the line, excluding any line break characters. + /// </summary> + public string GetText() { return this.Image.GetText(this.Extent); } + + /// <summary> + /// The text of the line, including any line break characters. + /// </summary> + public string GetTextIncludingLineBreak() { return this.Image.GetText(this.ExtentIncludingLineBreak); } + + /// <summary> + /// The string consisting of the line break characters (if any) at the + /// end of the line. Has zero length for the last line in the buffer. + /// </summary> + public string GetLineBreakText() { return this.Image.GetText(new Span(this.Extent.End, this.LineBreakLength)); } + + public override int GetHashCode() + { + return (this.Image != null) ? (this.LineNumber ^ this.Image.GetHashCode()) : 0; + } + + public override bool Equals(object obj) + { + if (obj is TextImageLine) + { + var other = (TextImageLine)obj; + return this.Equals(other); + } + + return false; + } + + public bool Equals(TextImageLine other) + { + return (other.Image == this.Image) && (other.LineNumber == this.LineNumber); + } + + public static bool operator ==(TextImageLine left, TextImageLine right) + { + return left.Equals(right); + } + + public static bool operator !=(TextImageLine left, TextImageLine right) + { + return !left.Equals(right); + } + + public override string ToString() + { + return (this.Image == null) + ? nameof(Invalid) + : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{0}[{1}, {2}+{3}]", + this.Image.Version?.VersionNumber, this.Extent.Start, this.Extent.End, this.LineBreakLength); + } + } +} diff --git a/src/Text/Def/TextData/Model/VersionedPosition.cs b/src/Text/Def/TextData/Model/VersionedPosition.cs new file mode 100644 index 0000000..8f9e036 --- /dev/null +++ b/src/Text/Def/TextData/Model/VersionedPosition.cs @@ -0,0 +1,90 @@ +// +// 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 +{ + using System; + + /// <summary> + /// Describes a location in a specific <see cref="ITextImageVersion"/>. + /// </summary> + public struct VersionedPosition : IEquatable<VersionedPosition> + { + public readonly ITextImageVersion Version; + public readonly int Position; + + public readonly static VersionedPosition Invalid = new VersionedPosition(); + + public VersionedPosition(ITextImageVersion version, int position) + { + if (version == null) + { + throw new ArgumentNullException(nameof(version)); + } + + if ((position < 0) || (position > version.Length)) + { + throw new ArgumentOutOfRangeException(nameof(position)); + } + + this.Version = version; + this.Position = position; + } + + public static implicit operator int(VersionedPosition position) + { + return position.Position; + } + + public VersionedPosition TranslateTo(ITextImageVersion other, PointTrackingMode mode) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + return new VersionedPosition(other, other.TrackTo(this, mode)); + } + + public override int GetHashCode() + { + return (this.Version != null) ? (this.Position ^ this.Version.GetHashCode()) : 0; + } + + public override bool Equals(object obj) + { + if (obj is VersionedPosition) + { + var other = (VersionedPosition)obj; + return this.Equals(other); + } + + return false; + } + + public bool Equals(VersionedPosition other) + { + return (other.Version == this.Version) && (other.Position == this.Position); + } + + public static bool operator ==(VersionedPosition left, VersionedPosition right) + { + return left.Equals(right); + } + + public static bool operator !=(VersionedPosition left, VersionedPosition right) + { + return !left.Equals(right); + } + + public override string ToString() + { + return (this.Version == null) + ? nameof(Invalid) + : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{1}_{2}", + this.Version.VersionNumber, + this.Position); + } + } +} diff --git a/src/Text/Def/TextData/Model/VersionedSpan.cs b/src/Text/Def/TextData/Model/VersionedSpan.cs new file mode 100644 index 0000000..67de693 --- /dev/null +++ b/src/Text/Def/TextData/Model/VersionedSpan.cs @@ -0,0 +1,90 @@ +// +// 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 +{ + using System; + + /// <summary> + /// Describes a span in a specific <see cref="ITextImageVersion"/>. + /// </summary> + public struct VersionedSpan : IEquatable<VersionedSpan> + { + public readonly ITextImageVersion Version; + public readonly Span Span; + + public readonly static VersionedSpan Invalid = new VersionedSpan(); + + public VersionedSpan(ITextImageVersion version, Span span) + { + if (version == null) + { + throw new ArgumentNullException(nameof(version)); + } + + if (span.End > version.Length) + { + throw new ArgumentOutOfRangeException(nameof(span)); + } + + this.Version = version; + this.Span = span; + } + + public static implicit operator Span(VersionedSpan span) + { + return span.Span; + } + + public VersionedSpan TranslateTo(ITextImageVersion other, SpanTrackingMode mode) + { + if (other == null) + { + throw new ArgumentNullException(nameof(other)); + } + + return new VersionedSpan(other, other.TrackTo(this, mode)); + } + + public override int GetHashCode() + { + return (this.Version != null) ? (this.Span.GetHashCode() ^ this.Version.GetHashCode()) : 0; + } + + public override bool Equals(object obj) + { + if (obj is VersionedSpan) + { + var other = (VersionedSpan)obj; + return this.Equals(other); + } + + return false; + } + + public bool Equals(VersionedSpan other) + { + return (other.Version == this.Version) && (other.Span == this.Span); + } + + public static bool operator ==(VersionedSpan left, VersionedSpan right) + { + return left.Equals(right); + } + + public static bool operator !=(VersionedSpan left, VersionedSpan right) + { + return !left.Equals(right); + } + + public override string ToString() + { + return (this.Version == null) + ? nameof(Invalid) + : string.Format(System.Globalization.CultureInfo.CurrentCulture, "v{1}_{2}", + this.Version.VersionNumber, + this.Span); + } + } +} diff --git a/src/Text/Def/TextUI/Adornments/BlockContext.cs b/src/Text/Def/TextUI/Adornments/BlockContext.cs deleted file mode 100644 index b74d188..0000000 --- a/src/Text/Def/TextUI/Adornments/BlockContext.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Microsoft.VisualStudio.Text.Adornments; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Tagging; -using System; - -namespace Microsoft.VisualStudio.Text.Adornments -{ - /// <summary> - /// An implementation of <see cref="IBlockContext"/>. - /// </summary> - public class BlockContext : IBlockContext - { - private readonly IBlockTag _blockTag; - private readonly ITextView _view; - private readonly object _content; - /// <summary> - /// Initializes a new instance of <see cref="IBlockContext"/> with the specified block tag. - /// </summary> - /// <param name="blockTag">The block tag associated with the structural block.</param> - /// <param name="view">The text view associated with the structural block.</param> - /// <param name="content">The content, including hiearchical parent statements, to be displayed in the tooltip.</param> - public BlockContext(IBlockTag blockTag, ITextView view, object content) - { - if (blockTag == null) - throw new ArgumentNullException("blockTag"); - - if (view == null) - throw new ArgumentNullException("view"); - - if (content == null) - throw new ArgumentNullException("content"); - - _blockTag = blockTag; - _view = view; - _content = content; - } - - /// <summary> - /// Gets the block tag associated with this context. - /// </summary> - public IBlockTag BlockTag - { - get { return _blockTag; } - } - - /// <summary> - /// Gets the text view associated with this context. - /// </summary> - public ITextView TextView - { - get { return _view; } - } - - /// <summary> - /// Gets the content that will be displayed in the tool tip, including hierarchical parent content. - /// </summary> - public object Content - { - get { return _content; } - } - } -} diff --git a/src/Text/Def/TextUI/Adornments/IBlockContext.cs b/src/Text/Def/TextUI/Adornments/IBlockContext.cs deleted file mode 100644 index 229eaf1..0000000 --- a/src/Text/Def/TextUI/Adornments/IBlockContext.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Tagging; - -namespace Microsoft.VisualStudio.Text.Adornments -{ - /// <summary> - /// Defines the block context used to display structural block information. - /// </summary> - public interface IBlockContext - { - /// <summary> - /// Gets the content that will be displayed in the tool tip, including hierarchical parent content. - /// </summary> - object Content { get; } - - /// <summary> - /// Gets the block tag associated with this context. - /// </summary> - IBlockTag BlockTag { get; } - - /// <summary> - /// Gets the text view associated with this context. - /// </summary> - ITextView TextView { get; } - } -} diff --git a/src/Text/Def/TextUI/Adornments/IBlockContextProvider.cs b/src/Text/Def/TextUI/Adornments/IBlockContextProvider.cs deleted file mode 100644 index 674881a..0000000 --- a/src/Text/Def/TextUI/Adornments/IBlockContextProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.VisualStudio.Text.Adornments -{ - /// <summary> - /// Creates a <see cref="IBlockContextSource"/> for a given buffer. - /// </summary> - /// <remarks>This is a MEF component part, and should be exported as follows: - /// [Export(typeof(IBlockContextProvider))] - /// Component exporters must add the Name and Order attribute to define the order of the provider in the provider chain. - /// </remarks> - public interface IBlockContextProvider - { - /// <summary> - /// Creates a block context source for the given text buffer. - /// </summary> - /// <param name="textBuffer">The text buffer for which to create a provider.</param> - /// <param name="token">The cancelation token for this asynchronous method call.</param> - /// <returns>A valid <see cref="IBlockContextSource" /> instance, or null if none could be created.</returns> - Task<IBlockContextSource> TryCreateBlockContextSourceAsync(ITextBuffer textBuffer, CancellationToken token); - } -} diff --git a/src/Text/Def/TextUI/Adornments/IBlockContextSource.cs b/src/Text/Def/TextUI/Adornments/IBlockContextSource.cs deleted file mode 100644 index bcb2bb5..0000000 --- a/src/Text/Def/TextUI/Adornments/IBlockContextSource.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Text.Tagging; -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.VisualStudio.Text.Adornments -{ - /// <summary> - /// Provides content for structural block tool tips for a given <see cref="IBlockTag"/>. - /// </summary> - public interface IBlockContextSource : IDisposable - { - /// <summary> - /// Gets the contexts for the given block tag. - /// </summary> - /// <param name="blockTag">The block tag for which the context is requested.</param> - /// <param name="view">The text view associated with the current context.</param> - /// <param name="token">The cancellation token for this asynchronous method call.</param> - /// <returns>The <see cref="IBlockContext" /> to be displayed in the tool tip.</returns> - Task<IBlockContext> GetBlockContextAsync(IBlockTag blockTag, ITextView view, CancellationToken token); - } -} diff --git a/src/Text/Def/TextUI/Adornments/IStructureContextSource.cs b/src/Text/Def/TextUI/Adornments/IStructureContextSource.cs new file mode 100644 index 0000000..8f5688e --- /dev/null +++ b/src/Text/Def/TextUI/Adornments/IStructureContextSource.cs @@ -0,0 +1,32 @@ +// +// 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.Adornments +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.VisualStudio.Text.UI.Adornments; + + /// <summary> + /// Provides context for structural block tool tips for a given sequence + /// of <see cref="IStructureElement"/>s. + /// </summary> + public interface IStructureContextSource : IDisposable + { + /// <summary> + /// Gets the context for the given structure tags. + /// </summary> + /// <param name="elements">The structure tags to get context for.</param> + /// <param name="token">The cancellation token for this asynchronous method call.</param> + /// <returns>The object to be displayed in the structure tool tip.</returns> + /// <remarks> + /// If the object returned by this method implements ITextView, ITextView.Close() is called + /// when the tooltip is dismissed. + /// </remarks> + Task<object> GetStructureContextAsync(IEnumerable<IStructureElement> elements, CancellationToken token); + } +} diff --git a/src/Text/Def/TextUI/Adornments/IStructureContextSourceProvider.cs b/src/Text/Def/TextUI/Adornments/IStructureContextSourceProvider.cs new file mode 100644 index 0000000..1807cb8 --- /dev/null +++ b/src/Text/Def/TextUI/Adornments/IStructureContextSourceProvider.cs @@ -0,0 +1,29 @@ +// +// 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.Adornments +{ + using Microsoft.VisualStudio.Text.Editor; + + /// <summary> + /// Creates an <see cref="IStructureContextSource"/> for a given buffer. + /// </summary> + /// <remarks>This is a MEF component part, and should be exported as follows: + /// [Export(typeof(IStructureContextSourceProvider))] + /// [Name("MyProviderName")] + /// [ContentType("MyContentTypeName")] + /// [Order(Before = "Foo", After = "Bar")] + /// Component exporters must add the Name and Order attribute to define the order of the provider in the provider chain. + /// </remarks> + public interface IStructureContextSourceProvider + { + /// <summary> + /// Creates a structure context source for the given text view. + /// </summary> + /// <param name="textView">The text view for which to create an <see cref="IStructureContextSource"/>.</param> + /// <returns>A valid <see cref="IStructureContextSource" /> instance, or null if none could be created.</returns> + IStructureContextSource CreateStructureContextSource(ITextView textView); + } +} diff --git a/src/Text/Def/TextUI/Adornments/IStructureElement.cs b/src/Text/Def/TextUI/Adornments/IStructureElement.cs new file mode 100644 index 0000000..06fd06f --- /dev/null +++ b/src/Text/Def/TextUI/Adornments/IStructureElement.cs @@ -0,0 +1,84 @@ +// +// 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.UI.Adornments +{ + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Adornments; + + /// <summary> + /// Represents a unit of structure in a code <see cref="ITextBuffer"/>. + /// </summary> + /// <remarks> + /// This class is an immutable subtree of the code structure in the view. + /// </remarks> + public interface IStructureElement + { + /// <summary> + /// The <see cref="IStructureElement"/>s nested within this one. + /// </summary> + IReadOnlyList<IStructureElement> Children { get; } + + /// <summary> + /// The span of text at the top of the structural element. e.g.: 'if (true)' + /// </summary> + SnapshotSpan? HeaderSpan { get; } + + /// <summary> + /// The vertical span within which the structure guide line adornment should + /// be drawn. + /// </summary> + SnapshotSpan? GuideLineSpan { get; } + + /// <summary> + /// The span of text to collapse when the outlining adornment is invoked. + /// </summary> + SnapshotSpan? OutliningSpan { get; } + + /// <summary> + /// The full extent of the block, from the start of the header to the end of the guideline. + /// </summary> + SnapshotSpan ExtentSpan { get; } + + /// <summary> + /// The horizontal offset with which to align the structure guide line. + /// </summary> + SnapshotPoint? GuideLineHorizontalAnchorPoint { get; } + + /// <summary> + /// One of the <see cref="PredefinedStructureTypes"/>, indicating the semantics + /// of this structural element. + /// </summary> + string Type { get; } + + /// <summary> + /// Indicates whether or not this element should display a collapse adornment. + /// </summary> + bool IsCollapsible { get; } + + /// <summary> + /// Indicates whether or not this element should be collapsed at document open. + /// </summary> + bool IsDefaultCollapsed { get; } + + /// <summary> + /// Indicates whether or not this element is related to implementation of a method, + /// function, or property. + /// </summary> + bool IsImplementation { get; } + + /// <summary> + /// Gets the text to display in the collapse adornment. + /// </summary> + /// <returns>The text to display in the collapse adornment.</returns> + object GetCollapsedForm(); + + /// <summary> + /// Gets the text to display in the collapse adornment tooltip. + /// </summary> + /// <returns>The text displayed in the collapse adornment tooltip.</returns> + object GetCollapsedHintForm(); + } +} diff --git a/src/Text/Def/TextUI/Adornments/PredefinedStructureTagTypes.cs b/src/Text/Def/TextUI/Adornments/PredefinedStructureTagTypes.cs new file mode 100644 index 0000000..5260fb6 --- /dev/null +++ b/src/Text/Def/TextUI/Adornments/PredefinedStructureTagTypes.cs @@ -0,0 +1,73 @@ +// +// 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.Adornments +{ + /// <summary> + /// Enumerates the predefined structural block types. + /// </summary> + public static class PredefinedStructureTagTypes + { + /// <summary> + /// Represents structural blocks, with vertical line adornments displayed. + /// </summary> + public const string Structural = nameof(Structural); + + /// <summary> + /// Represents non-structural blocks, with no vertical line adornments + /// displayed, only expand and collapse. + /// </summary> + public const string Nonstructural = nameof(Nonstructural); + + /// <summary> + /// Represents a code comment, with vertical line adornments. + /// </summary> + public const string Comment = nameof(Comment); + + /// <summary> + /// Represents a PreprocessorRegion, with vertical line adornments. + /// </summary> + public const string PreprocessorRegion = nameof(PreprocessorRegion); + + /// <summary> + /// Represents an Import or Imports Block, with vertical line adornments. + /// </summary> + public const string Imports = nameof(Imports); + + /// <summary> + /// Represents a Namespace, with vertical line adornments. + /// </summary> + public const string Namespace = nameof(Namespace); + + /// <summary> + /// Represents a Type, with vertical line adornments. + /// </summary> + public const string Type = nameof(Type); + + /// <summary> + /// Represents a class Member, such as a method or property, with vertical line adornments. + /// </summary> + public const string Member = nameof(Member); + + /// <summary> + /// Represents a Statement, with vertical line adornments. + /// </summary> + public const string Statement = nameof(Statement); + + /// <summary> + /// Represents a Conditional, with vertical line adornments. + /// </summary> + public const string Conditional = nameof(Conditional); + + /// <summary> + /// Represents a Loop, with vertical line adornments. + /// </summary> + public const string Loop = nameof(Loop); + + /// <summary> + /// Represents an Expression, with vertical line adornments. + /// </summary> + public const string Expression = nameof(Expression); + } +} diff --git a/src/Text/Def/TextUI/Adornments/PredefinedStructureTypes.cs b/src/Text/Def/TextUI/Adornments/PredefinedStructureTypes.cs deleted file mode 100644 index b1146ae..0000000 --- a/src/Text/Def/TextUI/Adornments/PredefinedStructureTypes.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace Microsoft.VisualStudio.Text.Adornments -{ - /// <summary> - /// Enumerates the predefined structural block types. - /// </summary> - public static class PredefinedStructureTypes - { - /// <summary> - /// Represents structural blocks, with vertical line adornments displayed. - /// </summary> - public const string Structural = "Structural"; - - /// <summary> - /// Represents non-structural blocks, with no vertical line adornments - /// displayed, only expand and collapse. - /// </summary> - public const string Nonstructural = "Nonstructural"; - - public const string PropertyBlock = "PropertyBlock"; - public const string AccessorBlock = "AccessorBlock"; - public const string AnonymousMethodBlock = "AnonymousMethodBlock"; // i.e. lambda bodies - public const string Constructor = "Constructor"; - public const string Destructor = "Destructor"; - public const string Operator = "Operator"; - public const string Method = "Method"; - public const string Namespace = "Namespace"; - public const string Class = "Class"; - public const string Interface = "Interface"; - public const string Struct = "Struct"; - public const string TryCatchFinally = "TryCatchFinally"; - public const string Conditional = "Conditional"; // i.e. If statements, ‘switch’ statements.i.e conditionals+branches - public const string Case = "Case"; - public const string Loop = "Loop"; // i.e. While/For/Foreach/Do/Until (i.e.loops) - public const string Standalone = "Standalone"; // i.e. stand-alone {} used for scoping. - - public const string Lock = "Context"; // i.e. using/lock/checked/unchecked. Need a better name for this. - - public const string Unknown = "Unknown"; - } -} diff --git a/src/Text/Def/TextUI/Adornments/StructureAdornmentStyle.cs b/src/Text/Def/TextUI/Adornments/StructureAdornmentStyle.cs deleted file mode 100644 index f71fe11..0000000 --- a/src/Text/Def/TextUI/Adornments/StructureAdornmentStyle.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Windows.Media; - -namespace Microsoft.VisualStudio.Text.Adornments -{ - /// <summary> - /// Defines a set of properties that will be used to style the default structural block tool tip. - /// </summary> - /// <remarks> - /// This is a MEF component part, and should be exported with the following attributes: - /// [Export(typeof(StructureAdornmentStyle))] - /// [Name] - /// [Order] - /// </remarks> - public class StructureAdornmentStyle - { - /// <summary> - /// Gets a <see cref="Brush"/> that will be used to paint the borders in the structure block tool tip. - /// </summary> - public virtual Brush BorderBrush { get; protected set; } - - /// <summary> - /// Gets a <see cref="Brush"/> that will be used to paint the background of the structure block tool tip. - /// </summary> - public virtual Brush BackgroundBrush { get; protected set; } - } -} diff --git a/src/Text/Def/TextUI/DifferenceViewer/DifferenceHighlightMode3.cs b/src/Text/Def/TextUI/DifferenceViewer/DifferenceHighlightMode3.cs new file mode 100644 index 0000000..6067159 --- /dev/null +++ b/src/Text/Def/TextUI/DifferenceViewer/DifferenceHighlightMode3.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +namespace Microsoft.VisualStudio.Text.Differencing +{ + + /// <summary> + /// The highlight mode for this <see cref="IDifferenceViewer"/>. + /// </summary> + public enum DifferenceHighlightMode3 + { + /// <summary> + /// In this mode, line differences should be displayed only to the last character on each line. + /// </summary> + CodeContour = DifferenceHighlightMode.CodeContour, + + /// <summary> + /// In this mode, line differences should be displayed so that they take up the entire width of the viewport. + /// </summary> + WholeLine = DifferenceHighlightMode.WholeLine, + + /// <summary> + /// In this mode, line and word differences are shown as outlined rectangles. + /// </summary> + BlockOutline = DifferenceHighlightMode2.BlockOutline, + + /// <summary> + /// In this mode, line differences are shown as outlined rectangles and + /// word differences are shown as colored rectangles. + /// </summary> + BlockOutlineWithWordDiffs + } +} diff --git a/src/Text/Def/TextUI/Editor/ConnectionReason.cs b/src/Text/Def/TextUI/Editor/ConnectionReason.cs new file mode 100644 index 0000000..194842a --- /dev/null +++ b/src/Text/Def/TextUI/Editor/ConnectionReason.cs @@ -0,0 +1,27 @@ +// +// 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> + /// Defines the reasons for connecting or disconnecting a text buffer and a text view. + /// </summary> + public enum ConnectionReason + { + /// <summary> + /// The <see cref="ITextView"/> has been opened or closed. + /// </summary> + TextViewLifetime, + + /// <summary> + /// The <see cref="Microsoft.VisualStudio.Utilities.IContentType"/> of the subject buffer has changed. + /// </summary> + ContentTypeChange, + + /// <summary> + /// A buffer has been added to or removed from <see cref="Microsoft.VisualStudio.Text.Projection.IBufferGraph"/>. + /// </summary> + BufferGraphChange + } +} diff --git a/src/Text/Def/TextUI/Editor/ITextViewConnectionListener.cs b/src/Text/Def/TextUI/Editor/ITextViewConnectionListener.cs new file mode 100644 index 0000000..4390964 --- /dev/null +++ b/src/Text/Def/TextUI/Editor/ITextViewConnectionListener.cs @@ -0,0 +1,47 @@ +// +// 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 +{ + using System.Collections.ObjectModel; + using System.Collections.Generic; + + /// <summary> + /// Listens to text buffers of a particular content type to find out when they are opened or closed + /// in the text editor. + /// </summary> + /// <remarks>This is a MEF component part, and should be exported with the following attribute: + /// [Export(typeof(ITextViewConnectionListener))] + /// [ContentType("...")] + /// [TextViewRole("...")] + /// </remarks> + public interface ITextViewConnectionListener + { + /// <summary> + /// Called when one or more <see cref="ITextBuffer"/> objects of the appropriate <see cref="Microsoft.VisualStudio.Utilities.IContentType"/> are connected to a <see cref="ITextView"/>. + /// </summary> + /// <remarks> + /// A connection can occur at one of three times: (1) when the view is first created; (2) when the buffer becomes a member of the + /// <see cref="Microsoft.VisualStudio.Text.Projection.IBufferGraph"/> for the view; or (3) when the + /// <see cref="Microsoft.VisualStudio.Utilities.IContentType"/> of the buffer changes. + /// </remarks> + /// <param name="textView">The <see cref="ITextView"/> to which the subject buffers are being connected.</param> + /// <param name="reason">The cause of the connection.</param> + /// <param name="subjectBuffers">The non-empty list of <see cref="ITextBuffer"/> objects with matching + /// content types.</param> + void SubjectBuffersConnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers); + + /// <summary> + /// Called when one or more <see cref="ITextBuffer"/> objects no longer satisfy the conditions for being included in the subject buffers. + /// </summary> + /// <remarks> + /// Text buffers can be disconnected when they are removed as source buffers of some projection buffer, + /// or when their content type changes, or when the <see cref="ITextView"/> is closed. + /// </remarks> + /// <param name="textView">The <see cref="ITextView"/> from which the subject buffers are being disconnected.</param> + /// <param name="reason">The cause of the disconnection.</param> + /// <param name="subjectBuffers">The non-empty list of <see cref="ITextBuffer"/> objects.</param> + void SubjectBuffersDisconnected(ITextView textView, ConnectionReason reason, IReadOnlyCollection<ITextBuffer> subjectBuffers); + } +} diff --git a/src/Text/Def/TextUI/Editor/ITextViewCreationListener.cs b/src/Text/Def/TextUI/Editor/ITextViewCreationListener.cs new file mode 100644 index 0000000..b6427b7 --- /dev/null +++ b/src/Text/Def/TextUI/Editor/ITextViewCreationListener.cs @@ -0,0 +1,23 @@ +// +// 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> + /// Listens for when <see cref="ITextView"/>s are created. + /// </summary> + /// <remarks>This is a MEF component part, and should be exported with the following attribute: + /// [Export(typeof(ITextViewCreationListener))] + /// [ContentType("...")] + /// [TextViewRole("...")] + /// </remarks> + public interface ITextViewCreationListener + { + /// <summary> + /// Called when a text view having matching roles is created over a text data model having a matching content type. + /// </summary> + /// <param name="textView">The newly created text view.</param> + void TextViewCreated(ITextView textView); + } +} diff --git a/src/Text/Def/TextUI/Tags/BlockTag.cs b/src/Text/Def/TextUI/Tags/BlockTag.cs deleted file mode 100644 index ebffbdf..0000000 --- a/src/Text/Def/TextUI/Tags/BlockTag.cs +++ /dev/null @@ -1,98 +0,0 @@ -namespace Microsoft.VisualStudio.Text.Tagging -{ - using Microsoft.VisualStudio.Text.Adornments; - - /// <summary> - /// An implementation of <see cref="IBlockTag" />. - /// </summary> - public abstract class BlockTag : IBlockTag - { - public BlockTag(SnapshotSpan span, SnapshotSpan statementSpan, IBlockTag parent, string type, bool isCollapsible, bool isDefaultCollapsed, bool isImplementation, object collapsedForm, object collapsedHintForm) - { - this.Span = span; - this.Level = (parent == null) ? 0 : (parent.Level + 1); - this.StatementSpan = statementSpan; - this.Parent = parent; - this.Type = type; - this.IsCollapsible = isCollapsible; - this.IsDefaultCollapsed = isDefaultCollapsed; - this.IsImplementation = isImplementation; - this.CollapsedForm = collapsedForm; - this.CollapsedHintForm = collapsedHintForm; - } - - /// <summary> - /// Gets the span of the structural block. - /// </summary> - public virtual SnapshotSpan Span { get; set; } - - /// <summary> - /// Gets the level of nested-ness of the structural block. - /// </summary> - public virtual int Level { get; } - - /// <summary> - /// Gets the span of the statement that control the structral block. - /// </summary> - /// <remarks> - /// <para> - /// For example, in the following snippet of code, - /// <code> - /// if (condition1 && - /// condition2) // comment - /// { - /// something; - /// } - /// </code> - /// this.StatementSpan would extend from the start of the "if" to the end of comment. - /// this.Span would extend from before the "{" to the end of the "}". - /// </para> - /// </remarks> - public virtual SnapshotSpan StatementSpan { get; set; } - - /// <summary> - /// Gets the hierarchical parent of the structural block. - /// </summary> - public virtual IBlockTag Parent { get; } - - /// <summary> - /// Determines the semantic type of the structural block. - /// <remarks> - /// See <see cref="PredefinedStructureTypes"/> for the canonical types. - /// Use <see cref="PredefinedStructureTypes.Nonstructural"/> for blocks that will not have any visible affordance - /// (but will be used for outlining). - /// </remarks> - /// </summary> - public virtual string Type { get; } - - /// <summary> - /// Determines whether a block can be collapsed. - /// </summary> - public virtual bool IsCollapsible { get; } - - /// <summary> - /// Determines whether a block is collapsed by default. - /// </summary> - public virtual bool IsDefaultCollapsed { get; } - - /// <summary> - /// Determines whether a block is an block region. - /// </summary> - /// <remarks> - /// Implementation blocks are the blocks of code following a method definition. - /// They are used for commands such as the Visual Studio Collapse to Definition command, - /// which hides the implementation block and leaves only the method definition exposed. - /// </remarks> - public virtual bool IsImplementation { get; } - - /// <summary> - /// Gets the data object for the collapsed UI. If the default is set, returns null. - /// </summary> - public virtual object CollapsedForm { get; } - - /// <summary> - /// Gets the data object for the collapsed UI tooltip. If the default is set, returns null. - /// </summary> - public virtual object CollapsedHintForm { get; } - } -} diff --git a/src/Text/Def/TextUI/Tags/IBlockTag.cs b/src/Text/Def/TextUI/Tags/IBlockTag.cs deleted file mode 100644 index 684db50..0000000 --- a/src/Text/Def/TextUI/Tags/IBlockTag.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace Microsoft.VisualStudio.Text.Tagging -{ - using Microsoft.VisualStudio.Text.Adornments; - - /// <summary> - /// Represents a structural code block, which is used for vertical structural line adornments. - /// </summary> - public interface IBlockTag : ITag - { - /// <summary> - /// Gets the span of the structural block. - /// </summary> - SnapshotSpan Span { get; } - - /// <summary> - /// Gets the level of nested-ness of the structural block. - /// </summary> - int Level { get; } - - /// <summary> - /// Gets the span of the statement that control the structral block. - /// </summary> - /// <remarks> - /// <para> - /// For example, in the following snippet of code, - /// <code> - /// if (condition1 && - /// condition2) // comment - /// { - /// something; - /// } - /// </code> - /// this.StatementSpan would extend from the start of the "if" to the end of comment. - /// this.Span would extend from before the "{" to the end of the "}". - /// </para> - /// </remarks> - SnapshotSpan StatementSpan { get; } - - /// <summary> - /// Gets the hierarchical parent of the structural block. - /// </summary> - IBlockTag Parent { get; } - - /// <summary> - /// Determines the semantic type of the structural block. - /// <remarks> - /// See <see cref="PredefinedStructureTypes"/> for the canonical types. - /// Use <see cref="PredefinedStructureTypes.Nonstructural"/> for blocks that will not have any visible affordance - /// (but will be used for outlining). - /// </remarks> - /// </summary> - string Type { get; } - - /// <summary> - /// Determines whether a block can be collapsed. - /// </summary> - bool IsCollapsible { get; } - - /// <summary> - /// Determines whether a block is collapsed by default. - /// </summary> - bool IsDefaultCollapsed { get; } - - /// <summary> - /// Determines whether a block is an implementation block. - /// </summary> - /// <remarks> - /// Implementation blocks are the blocks of code following a method definition. - /// They are used for commands such as the Visual Studio Collapse to Definition command, - /// which hides the implementation block and leaves only the method definition exposed. - /// </remarks> - bool IsImplementation { get; } - - /// <summary> - /// Gets the data object for the collapsed UI. If the default is set, returns null. - /// </summary> - object CollapsedForm { get; } - - /// <summary> - /// Gets the data object for the collapsed UI tooltip. If the default is set, returns null. - /// </summary> - object CollapsedHintForm { get; } - } -} diff --git a/src/Text/Def/TextUI/Tags/IStructureTag.cs b/src/Text/Def/TextUI/Tags/IStructureTag.cs new file mode 100644 index 0000000..5a7e950 --- /dev/null +++ b/src/Text/Def/TextUI/Tags/IStructureTag.cs @@ -0,0 +1,119 @@ +// +// 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.Tagging +{ + using Microsoft.VisualStudio.Text.Adornments; + + /// <summary> + /// Represents a structural code block, which is used for vertical structural line adornments + /// and outlining collapse regions. + /// </summary> + /// <remarks> + /// IStructureTag is the replacement for the <see cref="IBlockTag"/> which should not be used. + /// </remarks> + public interface IStructureTag : ITag + { + /// <summary> + /// The Snapshot from which this IStructureTag was generated. + /// </summary> + ITextSnapshot Snapshot { get; } + + /// <summary> + /// Gets the span containing the entire contents of the block (minus the block header). + /// This span will be collapsed or expanded when the block outlining adornment is invoked. + /// </summary> + /// <remarks> + /// If this parameter is null, block structure adornments will still be drawn as long as + /// the <see cref="GuideLineHorizontalAnchorPoint"/> and the <see cref="GuideLineSpan"/> are both provided, + /// however, no outlining adornment will be drawn. + /// </remarks> + Span? OutliningSpan { get; } + + /// <summary> + /// Gets the span of the statement that controls the structural block. + /// </summary> + /// <remarks> + /// <para> + /// For example, in the following snippet of code, + /// <code> + /// if (condition1 && + /// condition2) // comment + /// { + /// something; + /// } + /// </code> + /// this.HeaderSpan would extend from the start of the "if" to the end of comment. + /// this.OutliningSpan would extend from the end of "// comment" to the end of the "}". + /// </para> + /// <para> + /// If this parameter is null, block structure adornments will still be drawn as long as + /// the <see cref="OutliningSpan"/> is provided, or the <see cref="GuideLineHorizontalAnchorPoint"/> + /// and the <see cref="GuideLineSpan"/> are both provided. + /// </para> + /// </remarks> + Span? HeaderSpan { get; } + + /// <summary> + /// Gets the vertical span within which the block structure adornment will be drawn. + /// </summary> + /// <remarks> + /// For a block to have an adornment, it must not be of type <see cref="PredefinedStructureTagTypes.Nonstructural"/>, + /// and the implementer must also provide the GuideLineHorizontalAnchor. The adornment is drawn from the top of the + /// line containing the start of the span to the bottom of the line containing the bottom of the span. If null, + /// the GuideLineSpan is inferred from the OutliningSpan and the HeaderSpan. + /// </remarks> + Span? GuideLineSpan { get; } + + /// <summary> + /// Gets the point with which the block structure adornment will be horizontally aligned. + /// </summary> + /// <remarks> + /// This point can be on any line and is used solely for determining horizontal position. If null, + /// or if <see cref="GuideLineSpan"/> is null, this point is computed from the HeaderSpan and + /// OutliningSpan via heuristics. + /// </remarks> + int? GuideLineHorizontalAnchorPoint { get; } + + /// <summary> + /// Determines the semantic type of the structural block. + /// </summary> + /// <remarks> + /// See <see cref="PredefinedStructureTagTypes"/> for the canonical types. + /// Use <see cref="PredefinedStructureTagTypes.Nonstructural"/> for blocks that will not have any visible affordance + /// (but will be used for outlining). + /// </remarks> + string Type { get; } + + /// <summary> + /// Determines whether a block can be collapsed. + /// </summary> + bool IsCollapsible { get; } + + /// <summary> + /// Determines whether a block is collapsed by default. + /// </summary> + bool IsDefaultCollapsed { get; } + + /// <summary> + /// Determines whether a block is an implementation block. + /// </summary> + /// <remarks> + /// Implementation blocks are the blocks of code following a method definition. + /// They are used for commands such as the Visual Studio Collapse to Definition command, + /// which hides the implementation block and leaves only the method definition exposed. + /// </remarks> + bool IsImplementation { get; } + + /// <summary> + /// Gets the data object for the collapsed UI. If the default is set, returns null. + /// </summary> + object GetCollapsedForm(); + + /// <summary> + /// Gets the data object for the collapsed UI tooltip. If the default is set, returns null. + /// </summary> + object GetCollapsedHintForm(); + } +} diff --git a/src/Text/Def/TextUI/Tags/StructureTag.cs b/src/Text/Def/TextUI/Tags/StructureTag.cs new file mode 100644 index 0000000..1cea08c --- /dev/null +++ b/src/Text/Def/TextUI/Tags/StructureTag.cs @@ -0,0 +1,207 @@ +// +// 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.Tagging +{ + using System; + using Microsoft.VisualStudio.Text.Adornments; + using Microsoft.VisualStudio.Text.UI.Adornments; + + /// <summary> + /// An implementation of <see cref="IStructureTag" />. + /// </summary> + /// <remarks> + /// Using this class is the recommended way to create an instance of <see cref="IStructureElement"/> + /// for most purposes. IStructureTag is the replacement for the IBlockTag which should not be used. + /// </remarks> + public class StructureTag : IStructureTag + { + private readonly object collapsedForm; + private readonly object collapsedHintForm; + + /// <summary> + /// Constructs an instance of the <see cref="IStructureTag"/>. + /// </summary> + /// <remarks> + /// StructureTag is intended to replace <see cref="IBlockTag"/> and offers more explicit control + /// of the block structure adornments. This class operates on the pay-to-play principle, in that, + /// it will allow you to create a tag with just a subset of fields, but if a field is missing, it + /// will attempt to guess the missing fields from the information that it has. The most useful example + /// of this is to omit the GuideLineSpan and GuideLineHorizontalAnchor point to have the API guess + /// them from the HeaderSpan and StatementSpan indentation. If enough information is missing, the tag + /// does nothing. + /// </remarks> + /// <param name="snapshot">The snapshot used to generate this StructureTag.</param> + /// <param name="outliningSpan">The block contents, used to determine the collapse region.</param> + /// <param name="headerSpan">The control statement at the start of the block.</param> + /// <param name="guideLineSpan"> + /// The vertical span within which the block structure guide is drawn. + /// If this member is omitted, it is computed from the HeaderSpan and the OutliningSpan via heuristics.</param> + /// <param name="guideLineHorizontalAnchor"> + /// A point capturing the horizontal offset at which the guide is drawn. + /// If this member is omitted, it is computed from the HeaderSpan and the OutliningSpan via heuristics.</param> + /// <param name="type">The structure type of the block.</param> + /// <param name="isCollapsible">If true, block will have block adornments.</param> + /// <param name="isDefaultCollapsed">If true, block is collapsed by default.</param> + /// <param name="isImplementation">Defines whether or not the block defines a region following a function declaration.</param> + /// <param name="collapsedForm">The form the block appears when collapsed.</param> + /// <param name="collapsedHintForm">The form of the collapsed region tooltip.</param> + public StructureTag( + ITextSnapshot snapshot, + Span? outliningSpan = null, + Span? headerSpan = null, + Span? guideLineSpan = null, + int? guideLineHorizontalAnchor = null, + string type = null, + bool isCollapsible = false, + bool isDefaultCollapsed = false, + bool isImplementation = false, + object collapsedForm = null, + object collapsedHintForm = null) + { + if (snapshot == null) + { + throw new ArgumentNullException(nameof(snapshot)); + } + + if (outliningSpan != null && outliningSpan.Value.End > snapshot.Length) + { + throw new ArgumentOutOfRangeException(nameof(outliningSpan)); + } + + if (headerSpan != null && headerSpan.Value.End > snapshot.Length) + { + throw new ArgumentOutOfRangeException(nameof(headerSpan)); + } + + if (guideLineSpan != null && guideLineSpan.Value.End > snapshot.Length) + { + throw new ArgumentOutOfRangeException(nameof(guideLineSpan)); + } + + if (guideLineHorizontalAnchor != null && guideLineHorizontalAnchor.Value > snapshot.Length) + { + throw new ArgumentOutOfRangeException(nameof(guideLineHorizontalAnchor)); + } + + this.Snapshot = snapshot; + this.OutliningSpan = outliningSpan; + this.HeaderSpan = headerSpan; + this.GuideLineSpan = guideLineSpan; + this.GuideLineHorizontalAnchorPoint = guideLineHorizontalAnchor; + this.Type = type; + this.IsCollapsible = isCollapsible; + this.IsDefaultCollapsed = isDefaultCollapsed; + this.IsImplementation = isImplementation; + this.collapsedForm = collapsedForm; + this.collapsedHintForm = collapsedHintForm; + } + + /// <summary> + /// The Snapshot from which this structure tag was generated. + /// </summary> + public virtual ITextSnapshot Snapshot { get; } + + /// <summary> + /// Gets the span containing the entire contents of the block (minus the block header). + /// This span will be collapsed or expanded when the block outlining adornment is invoked. + /// </summary> + /// <remarks> + /// If this parameter is null, block structure adornments will still be drawn as long as + /// the <see cref="GuideLineHorizontalAnchorPoint"/> and the <see cref="GuideLineSpan"/> are both provided, + /// however, no outlining adornment will be drawn. + /// </remarks> + public virtual Span? OutliningSpan { get; set; } + + /// <summary> + /// Gets the span of the statement that controls the structural block. + /// </summary> + /// <remarks> + /// <para> + /// For example, in the following snippet of code, + /// <code> + /// if (condition1 && + /// condition2) // comment + /// { + /// something; + /// } + /// </code> + /// this.HeaderSpan would extend from the start of the "if" to the end of comment. + /// this.OutliningSpan would extend from the end of "// comment" to the end of the "}". + /// </para> + /// <para> + /// If this parameter is null, block structure adornments will still be drawn as long as + /// the <see cref="OutliningSpan"/> is provided, or the <see cref="GuideLineHorizontalAnchorPoint"/> + /// and the <see cref="GuideLineSpan"/> are both provided. + /// </para> + /// </remarks> + public virtual Span? HeaderSpan { get; } + + /// <summary> + /// Gets the point with which the block structure adornment will be horizontally aligned. + /// </summary> + /// <remarks> + /// This point can be on any line and is used solely for determining horizontal position. If null, + /// this point is computed from the HeaderSpan and OutliningSpan via heuristics. + /// </remarks> + public virtual int? GuideLineHorizontalAnchorPoint { get; } + + /// <summary> + /// Gets the vertical span within which the block structure adornment will be drawn. + /// </summary> + /// <remarks> + /// For a block to have an adornment, it must not be of type <see cref="PredefinedStructureTagTypes.Nonstructural"/>, + /// and the implementer must also provide the GuideLineHorizontalAnchor. The adornment is drawn from the top of the + /// line containing the start of the span to the bottom of the line containing the bottom of the span. If null, + /// the GuideLineSpan is inferred from the OutliningSpan and the HeaderSpan. + /// </remarks> + public virtual Span? GuideLineSpan { get; } + + /// <summary> + /// Determines the semantic type of the structural block. + /// </summary> + /// <remarks> + /// See <see cref="PredefinedStructureTagTypes"/> for the canonical types. + /// Use <see cref="PredefinedStructureTagTypes.Nonstructural"/> for blocks that will not have any visible affordance + /// (but will be used for outlining). + /// </remarks> + public virtual string Type { get; } + + /// <summary> + /// Determines whether or not a block can be collapsed. + /// </summary> + public virtual bool IsCollapsible { get; } + + /// <summary> + /// Determines whether a block is collapsed by default. + /// </summary> + public virtual bool IsDefaultCollapsed { get; } + + /// <summary> + /// Determines whether a StructureTag represents an implementation block region. + /// </summary> + /// <remarks> + /// Implementation blocks are the blocks of code following a method definition. + /// They are used for commands such as the Visual Studio Collapse to Definition command, + /// which hides the implementation block and leaves only the method definition exposed. + /// </remarks> + public virtual bool IsImplementation { get; } + + /// <summary> + /// Gets the data object for the collapsed UI. If the default is set, returns null. + /// </summary> + public virtual object GetCollapsedForm() + { + return this.collapsedForm; + } + + /// <summary> + /// Gets the data object for the collapsed UI tooltip. If the default is set, returns null. + /// </summary> + public virtual object GetCollapsedHintForm() + { + return this.collapsedHintForm; + } + } +} diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs b/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs new file mode 100644 index 0000000..7ccd6cb --- /dev/null +++ b/src/Text/Impl/ClassificationAggregator/ClassifierAggregator.cs @@ -0,0 +1,370 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Classification.Implementation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Threading; + + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Tagging; + + /// <summary> + /// Defines the aggregator for all the classifiers - and is available to the external world via a VisualStudio + /// Service + /// </summary> + internal sealed class ClassifierAggregator : IAccurateClassifier, IDisposable + { + #region Private Members + + private ITextBuffer _textBuffer; + private IClassificationTypeRegistryService _classificationTypeRegistry; + private IAccurateTagAggregator<IClassificationTag> _tagAggregator; + #endregion // Private Members + + #region Constructors + + internal ClassifierAggregator(ITextBuffer textBuffer, + IBufferTagAggregatorFactoryService bufferTagAggregatorFactory, + IClassificationTypeRegistryService classificationTypeRegistry) + { + // Validate. + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + if (bufferTagAggregatorFactory == null) + { + throw new ArgumentNullException("bufferTagAggregatorFactory"); + } + if (classificationTypeRegistry == null) + { + throw new ArgumentNullException("classificationTypeRegistry"); + } + + _textBuffer = textBuffer; + _classificationTypeRegistry = classificationTypeRegistry; + + // Create a tag aggregator that maps by content type, so we don't map through projection buffers that aren't "projection" content type. + _tagAggregator = bufferTagAggregatorFactory.CreateTagAggregator<IClassificationTag>(textBuffer, TagAggregatorOptions.MapByContentType) as IAccurateTagAggregator<IClassificationTag>; + _tagAggregator.BatchedTagsChanged += OnBatchedTagsChanged; + } + + internal ClassifierAggregator(ITextView textView, + IViewTagAggregatorFactoryService viewTagAggregatorFactory, + IClassificationTypeRegistryService classificationTypeRegistry) + { + // Validate. + if (textView == null) + { + throw new ArgumentNullException("textView"); + } + if (viewTagAggregatorFactory == null) + { + throw new ArgumentNullException("viewTagAggregatorFactory"); + } + if (classificationTypeRegistry == null) + { + throw new ArgumentNullException("classificationTypeRegistry"); + } + + _textBuffer = textView.TextBuffer; + _classificationTypeRegistry = classificationTypeRegistry; + + // Create a tag aggregator that maps by content type, so we don't map through projection buffers that aren't "projection" content type. + _tagAggregator = viewTagAggregatorFactory.CreateTagAggregator<IClassificationTag>(textView, TagAggregatorOptions.MapByContentType) as IAccurateTagAggregator<IClassificationTag>; + _tagAggregator.BatchedTagsChanged += OnBatchedTagsChanged; + } + + #endregion // Constructors + + #region Exposed Methods + + /// <summary> + /// Gets all classification spans that overlap the given range of text + /// </summary> + /// <param name="span"> + /// The span of text of interest + /// </param> + /// <returns> + /// A list of lists of ClassificationSpans that intersect with the given range + /// </returns> + public IList<ClassificationSpan> GetClassificationSpans(SnapshotSpan span) + { + return this.InternalGetClassificationSpans(span, _tagAggregator.GetTags(span)); + } + + public IList<ClassificationSpan> GetAllClassificationSpans(SnapshotSpan span, CancellationToken cancel) + { + return this.InternalGetClassificationSpans(span, _tagAggregator.GetAllTags(span, cancel)); + } + #endregion // Exposed Methods + + #region IDisposable + public void Dispose() + { + if (_tagAggregator != null) + { + _tagAggregator.BatchedTagsChanged -= OnBatchedTagsChanged; + _tagAggregator.Dispose(); + + _tagAggregator = null; + } + } + #endregion + + #region Public Events + + /// <summary> + /// Notifies a classification change. The aggregator also bubbles ClassificationChanged + /// events up from aggregated classifiers + /// </summary> + public event EventHandler<ClassificationChangedEventArgs> ClassificationChanged; + + #endregion // Public Events + + #region Event Handlers + + private void OnBatchedTagsChanged(object sender, BatchedTagsChangedEventArgs e) + { + var tempEvent = ClassificationChanged; + if (tempEvent != null) + { + foreach (var mappingSpan in e.Spans) + { + foreach (var span in mappingSpan.GetSpans(_textBuffer)) + { + tempEvent(this, new ClassificationChangedEventArgs(span)); + } + } + } + } + + #endregion // Event Handlers + + #region Private Helpers + + private IList<ClassificationSpan> InternalGetClassificationSpans(SnapshotSpan span, IEnumerable<IMappingTagSpan<IClassificationTag>> tags) + { + List<ClassificationSpan> allSpans = new List<ClassificationSpan>(); + foreach (var tagSpan in tags) + { + NormalizedSnapshotSpanCollection tagSpans = tagSpan.Span.GetSpans(span.Snapshot.TextBuffer); + for (int s = 0; s < tagSpans.Count; ++s) + { + allSpans.Add(new ClassificationSpan( + tagSpans[s].TranslateTo(span.Snapshot, SpanTrackingMode.EdgeExclusive), + tagSpan.Tag.ClassificationType)); + } + } + + return this.NormalizeClassificationSpans(span, allSpans); + } + + #region Private classes + struct PointData + { + public readonly bool IsStart; + public readonly int Position; + public readonly IClassificationType ClassificationType; + + public PointData(bool isStart, int position, IClassificationType classificationType) + { + this.IsStart = isStart; + this.Position = position; + this.ClassificationType = classificationType; + } + } + + class OpenSpanData + { + public readonly IClassificationType ClassificationType; + + public int Count = 1; // How many open classification spans with this classification type are there? + + public OpenSpanData(IClassificationType classificationType) + { + this.ClassificationType = classificationType; + } + }; + #endregion + + /// <summary> + /// Normalizes a list of classifications. Given an arbitrary set of ClassificationSpan lists, creates + /// a new list with each ClassificationSpan, sorted by position and with the appropriate transient + /// classification types for spans that had multiple classifications. + /// </summary> + /// <param name="spans">A list of lists that contain ClassificationSpans.</param> + /// <param name="requestedRange"> + /// The requested range of this call so results can be trimmed if there are any + /// classifiers that returned spans past the requested range they can be trimmed. + /// </param> + /// <returns>A sorted list of normalized spans.</returns> + /// <remarks> + /// Basic algorithm: + /// Create a list of the starting and ending points for each classification (clipped against requestedRange). + /// Sort the points list by position. If the positions are the same, starting points go before ending points (this + /// prevents a seam between two adjoining classifications of the same type). + /// + /// Make a list of the currently open spans. + /// Walk the points in order. + /// When encountering a starting point: + /// If there is already an open span of starting point's type, increment the count associated with the open span. + /// Otherwise, add a new classification span based on the currently open spans and then add an open span (based on the startpoint) to the list of open spans. + /// + /// When encountering an ending point: + /// Check the count for the open span of the ending point's type (there must be one): + /// If it is one, add a new classiciation based on the open spans and remove that open span from the list of open spans. + /// Otherwise, decrement the count associated with the open span. + /// </remarks> + private List<ClassificationSpan> NormalizeClassificationSpans(SnapshotSpan requestedRange, IList<ClassificationSpan> spans) + { + // Add the start and end points of each classification to one sorted list + List<PointData> points = new List<PointData>(spans.Count * 2); + int lastEndPoint = -1; + IClassificationType lastClassificationType = null; + bool requiresNormalization = false; + foreach (ClassificationSpan classification in spans) + { + // Only consider classifications that overlap the requested span + Span? span = requestedRange.Overlap(classification.Span.TranslateTo(requestedRange.Snapshot, SpanTrackingMode.EdgeExclusive)); + + if (span.HasValue) + { + // Use a <= test so that adjoining classifications will go through the normalizing process + // (so that adjoining classifications of the same type will be merged into a single + // classification span). + Span overlap = span.Value; + + if ((overlap.Start < lastEndPoint) || + ((overlap.Start == lastEndPoint) && (classification.ClassificationType == lastClassificationType))) + { + requiresNormalization = true; + } + + lastEndPoint = overlap.End; + lastClassificationType = classification.ClassificationType; + + points.Add(new PointData(true, overlap.Start, lastClassificationType)); + points.Add(new PointData(false, overlap.End, lastClassificationType)); + } + } + + List<ClassificationSpan> results = new List<ClassificationSpan>(); + if (!requiresNormalization) + { + // The generated points list is already sorted, so simple generate a list of classifications from it without normalizing + // (we can't use spans since its classifications have not been clipped to requestedRange). + for (int i = 1; (i < points.Count); i += 2) + { + PointData startPoint = points[i - 1]; + PointData endPoint = points[i]; + + results.Add(new ClassificationSpan(new SnapshotSpan(requestedRange.Snapshot, startPoint.Position, endPoint.Position - startPoint.Position), + startPoint.ClassificationType)); + } + } + else + { + points.Sort(Compare); + + int nextSpanStart = 0; + + List<OpenSpanData> openSpans = new List<OpenSpanData>(); + for (int p = 0; p < points.Count; ++p) + { + PointData point = points[p]; + OpenSpanData openSpanData = null; + // write this little search explicitly instead of using the Find method, because allocating + // a delegate here in a high-frequency loop allocates noticeable amounts of memory + for (int os = 0; os < openSpans.Count; ++os) + { + if (openSpans[os].ClassificationType == point.ClassificationType) + { + openSpanData = openSpans[os]; + break; + } + } + if (point.IsStart) + { + if (openSpanData != null) + { + ++(openSpanData.Count); + } + else + { + if ((openSpans.Count > 0) && (point.Position > nextSpanStart)) + { + this.AddClassificationSpan(openSpans, requestedRange.Snapshot, nextSpanStart, point.Position, results); + } + nextSpanStart = point.Position; + openSpans.Add(new OpenSpanData(point.ClassificationType)); + } + } + else + { + if (openSpanData.Count > 1) + { + --(openSpanData.Count); + } + else + { + if (point.Position > nextSpanStart) + { + this.AddClassificationSpan(openSpans, requestedRange.Snapshot, nextSpanStart, point.Position, results); + } + nextSpanStart = point.Position; + openSpans.Remove(openSpanData); + } + } + } + } + + // Return our list of aggregated spans. + return results; + } + + private int Compare(PointData a, PointData b) + { + if (a.Position == b.Position) + return (b.IsStart.CompareTo(a.IsStart)); // startpoints go before end points when positions are tied + else + return (a.Position - b.Position); + } + + /// <summary> + /// Add a classification span to <paramref name="results"/> that corresponds to the open classification spans in <paramref name="openSpans"/> + /// between <paramref name="start"/> and <paramref name="end"/>. + /// </summary> + private void AddClassificationSpan(List<OpenSpanData> openSpans, ITextSnapshot snapshot, int start, int end, IList<ClassificationSpan> results) + { + IClassificationType classificationType; + + if (openSpans.Count == 1) + { + // If there is only one classification type, then don't create a transient type. + classificationType = openSpans[0].ClassificationType; + } + else + { + // There is more than one classification type, create a transient type that corresponds + // to all the open classificaiton types. + List<IClassificationType> classifications = new List<IClassificationType>(openSpans.Count); + foreach (OpenSpanData osd in openSpans) + classifications.Add(osd.ClassificationType); + + classificationType = _classificationTypeRegistry.CreateTransientClassificationType(classifications); + } + + results.Add(new ClassificationSpan(new SnapshotSpan(snapshot, start, end - start), + classificationType)); + } + #endregion + } +} diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierAggregatorService.cs b/src/Text/Impl/ClassificationAggregator/ClassifierAggregatorService.cs new file mode 100644 index 0000000..bad3402 --- /dev/null +++ b/src/Text/Impl/ClassificationAggregator/ClassifierAggregatorService.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Classification.Implementation +{ + using System; + using System.ComponentModel.Composition; + + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Tagging; + + /// <summary> + /// Exports the <see cref="IClassifierAggregatorService"/> component. + /// </summary> + [Export(typeof(IClassifierAggregatorService))] + [Export(typeof(IViewClassifierAggregatorService))] + internal sealed class ClassifierAggregatorService : IClassifierAggregatorService, IViewClassifierAggregatorService + { + [Import] + internal IBufferTagAggregatorFactoryService _bufferTagAggregatorFactory { get; set; } + + [Import] + internal IViewTagAggregatorFactoryService _viewTagAggregatorFactory { get; set; } + + [Import] + internal IClassificationTypeRegistryService _classificationTypeRegistry { get; set; } + + public IClassifier GetClassifier(ITextBuffer textBuffer) + { + return new ClassifierAggregator(textBuffer, _bufferTagAggregatorFactory, _classificationTypeRegistry); + } + + public IClassifier GetClassifier(ITextView textView) + { + return new ClassifierAggregator(textView, _viewTagAggregatorFactory, _classificationTypeRegistry); + } + } +} diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs b/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs new file mode 100644 index 0000000..653e768 --- /dev/null +++ b/src/Text/Impl/ClassificationAggregator/ClassifierTagger.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Classification.Implementation +{ + using System; + using System.Collections.Generic; + using System.Threading; + using Microsoft.VisualStudio.Text.Tagging; + + internal class ClassifierTagger : IAccurateTagger<ClassificationTag>, IDisposable + { + internal IList<IClassifier> Classifiers { get; private set; } + + internal ClassifierTagger(IList<IClassifier> classifiers) + { + Classifiers = classifiers; + + foreach(var classifier in classifiers) + { + classifier.ClassificationChanged += OnClassificationChanged; + } + } + + #region ITagger<ClassificationTag> members + public IEnumerable<ITagSpan<ClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans) + { + foreach (IClassifier classifier in Classifiers) + { + foreach(var snapshotSpan in spans) + { + foreach(var classificationSpan in classifier.GetClassificationSpans(snapshotSpan)) + { + yield return new TagSpan<ClassificationTag>( + classificationSpan.Span, + new ClassificationTag(classificationSpan.ClassificationType)); + } + } + } + } + + public event EventHandler<SnapshotSpanEventArgs> TagsChanged; + #endregion + + #region IAccurateTagger<ClassificationTag> members + public IEnumerable<ITagSpan<ClassificationTag>> GetAllTags(NormalizedSnapshotSpanCollection spans, CancellationToken cancel) + { + foreach (IClassifier classifier in Classifiers) + { + IAccurateClassifier classifier2 = classifier as IAccurateClassifier; + + foreach (var snapshotSpan in spans) + { + foreach (var classificationSpan in (classifier2 != null) + ? classifier2.GetAllClassificationSpans(snapshotSpan, cancel) + : classifier.GetClassificationSpans(snapshotSpan)) + { + yield return new TagSpan<ClassificationTag>( + classificationSpan.Span, + new ClassificationTag(classificationSpan.ClassificationType)); + } + } + } + } + #endregion + + #region IDisposable members + + public void Dispose() + { + foreach(var classifier in Classifiers) + { + classifier.ClassificationChanged -= OnClassificationChanged; + } + + Classifiers.Clear(); + + GC.SuppressFinalize(this); + } + + #endregion + + /// <summary> + /// Handles the classification Changed events from the classifiers by turning + /// them into TagsChanged events. + /// </summary> + void OnClassificationChanged(object sender, ClassificationChangedEventArgs e) + { + var tempEvent = TagsChanged; + if (tempEvent != null) + tempEvent(this, new SnapshotSpanEventArgs(e.ChangeSpan)); + } + } +} diff --git a/src/Text/Impl/ClassificationAggregator/ClassifierTaggerProvider.cs b/src/Text/Impl/ClassificationAggregator/ClassifierTaggerProvider.cs new file mode 100644 index 0000000..08ebac3 --- /dev/null +++ b/src/Text/Impl/ClassificationAggregator/ClassifierTaggerProvider.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Classification.Implementation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(ITaggerProvider))] + [ContentType("any")] + [TagType(typeof(ClassificationTag))] + internal class ClassifierTaggerProvider : ITaggerProvider + { + [ImportMany(typeof(IClassifierProvider))] + internal List<Lazy<IClassifierProvider, INamedContentTypeMetadata>> _classifierProviders { get; set; } + + [Import] + internal GuardedOperations _guardedOperations { get; set; } + + [Import] + private IContentTypeRegistryService ContentTypeRegistryService { get; set; } + + public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag + { + var classifiers = + _guardedOperations.InvokeEligibleFactories + (_classifierProviders, (IClassifierProvider provider) => (provider.GetClassifier(buffer)), buffer.ContentType, this.ContentTypeRegistryService, this); + + return classifiers.Count > 0 + ? new ClassifierTagger(classifiers) as ITagger<T> + : null; + } + } +} diff --git a/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs b/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs new file mode 100644 index 0000000..35a5835 --- /dev/null +++ b/src/Text/Impl/ClassificationAggregator/ProjectionWorkaround.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Classification.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(ITaggerProvider))] + [ContentType("projection")] + [TagType(typeof(ClassificationTag))] + internal class ProjectionWorkaroundProvider : ITaggerProvider + { + [Import] + internal IDifferenceService diffService { get; set; } + + public ITagger<T> CreateTagger<T>(ITextBuffer buffer) where T : ITag + { + IProjectionBuffer projectionBuffer = buffer as IProjectionBuffer; + if (projectionBuffer == null) + return null; + + return new ProjectionWorkaroundTagger(projectionBuffer, diffService) as ITagger<T>; + } + } + + /// <summary> + /// This is a workaround for projection buffers. Currently, the way our projection buffer + /// implementation does "minimal" updating is by using *lexical* differencing. The + /// problem with this approach is that if you replace a span in a projection buffer + /// with a lexically equivalent span from a *different* buffer, the projection buffer + /// will event that no changes have been made. + /// </summary> + internal class ProjectionWorkaroundTagger : ITagger<ClassificationTag> + { + IProjectionBuffer ProjectionBuffer { get; set; } + IDifferenceService diffService; + + internal ProjectionWorkaroundTagger(IProjectionBuffer projectionBuffer, IDifferenceService diffService) + { + this.ProjectionBuffer = projectionBuffer; + this.diffService = diffService; + this.ProjectionBuffer.SourceBuffersChanged += SourceSpansChanged; + } + + #region ITagger<ClassificationTag> members + public IEnumerable<ITagSpan<ClassificationTag>> GetTags(NormalizedSnapshotSpanCollection spans) + { + yield break; + } + + public event EventHandler<SnapshotSpanEventArgs> TagsChanged; + + #endregion + + #region Source span differencing + change event + private void SourceSpansChanged(object sender, ProjectionSourceSpansChangedEventArgs e) + { + if (e.Changes.Count == 0) + { + // If there weren't text changes, but there were span changes, then + // send out a classification changed event over the spans that changed. + ProjectionSpanDifference difference = ProjectionSpanDiffer.DiffSourceSpans(this.diffService, e.Before, e.After); + int pos = 0; + int start = int.MaxValue; + int end = int.MinValue; + foreach (var diff in difference.DifferenceCollection) + { + pos += GetMatchSize(difference.DeletedSpans, diff.Before); + start = Math.Min(start, pos); + + // Now, for every span added in the new snapshot that replaced + // the deleted spans, add it to our span to raise changed events + // over. + for (int i = diff.Right.Start; i < diff.Right.End; i++) + { + pos += difference.InsertedSpans[i].Length; + } + + end = Math.Max(end, pos); + } + + if (start != int.MaxValue && end != int.MinValue) + { + RaiseTagsChangedEvent(new SnapshotSpan(e.After, Span.FromBounds(start, end))); + } + } + } + + private static int GetMatchSize(ReadOnlyCollection<SnapshotSpan> spans, Match match) + { + int size = 0; + if (match != null) + { + Span extent = match.Left; + for (int s = extent.Start; s < extent.End; ++s) + { + size += spans[s].Length; + } + } + return size; + } + + private void RaiseTagsChangedEvent(SnapshotSpan span) + { + var handler = TagsChanged; + if (handler != null) + { + handler(this, new SnapshotSpanEventArgs(span)); + } + } + + #endregion + } +} diff --git a/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs b/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs new file mode 100644 index 0000000..6f3409f --- /dev/null +++ b/src/Text/Impl/ClassificationType/ClassificationTypeImpl.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System.Collections.Generic; +using System.Linq; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Classification.Implementation +{ + internal class ClassificationTypeImpl : IClassificationType + { + private string name; + private FrugalList<IClassificationType> baseTypes; + + internal ClassificationTypeImpl(string name) + { + this.name = name; + } + + internal void AddBaseType(IClassificationType baseType) + { + if (this.baseTypes == null) + { + this.baseTypes = new FrugalList<IClassificationType>(); + } + + this.baseTypes.Add(baseType); + } + + public string Classification + { + get { return this.name; } + } + + public bool IsOfType(string type) + { + if (this.name == type) + return true; + else if (this.baseTypes != null) + { + foreach (IClassificationType baseType in this.baseTypes) + { + if ( baseType.IsOfType(type) ) + return true; + } + } + + return false; + } + + public IEnumerable<IClassificationType> BaseTypes + { + get { return (this.baseTypes != null) ? (IEnumerable<IClassificationType>)(this.baseTypes.AsReadOnly()) : Enumerable.Empty<IClassificationType>(); } + } + + public override string ToString() + { + return this.name; + } + } +} diff --git a/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs b/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs new file mode 100644 index 0000000..d890eb4 --- /dev/null +++ b/src/Text/Impl/ClassificationType/ClassificationTypeRegistryService.cs @@ -0,0 +1,253 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Classification.Implementation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Utilities; + using System.Collections; + + public interface IClassificationTypeDefinitionMetadata + { + string Name { get; } + [System.ComponentModel.DefaultValue(null)] + IEnumerable<string> BaseDefinition { get; } + } + + [Export(typeof(IClassificationTypeRegistryService))] + internal sealed class ClassificationTypeRegistryService : IClassificationTypeRegistryService + { + [ImportMany] + internal List<Lazy<ClassificationTypeDefinition, IClassificationTypeDefinitionMetadata>> _classificationTypeDefinitions { get; set; } + + [Export] + [Name("(TRANSIENT)")] + public ClassificationTypeDefinition transientClassificationType; + + [Export] + [Name("text")] + public ClassificationTypeDefinition textClassificationType; + + #region Private Members + private Dictionary<string, ClassificationTypeImpl> _classificationTypes; + private Dictionary<string, ClassificationTypeImpl> _transientClassificationTypes; + + #endregion // Private Members + + #region Public Members + public IClassificationType GetClassificationType(string type) + { + ClassificationTypeImpl classificationType = null; + + this.ClassificationTypes.TryGetValue(type, out classificationType); + + return classificationType; + } + + /// <summary> + /// Create a new classification type and add it to the registry. + /// </summary> + public IClassificationType CreateClassificationType(string type, IEnumerable<IClassificationType> baseTypes) + { + if (type == null) + { + throw new ArgumentNullException("type"); + } + + if (baseTypes == null) + { + throw new ArgumentNullException("baseTypes"); + } + if (ClassificationTypes.ContainsKey(type)) + { + throw new InvalidOperationException(LookUp.Strings.ClassificationAlreadyAdded); + } + + // Use the non-canonical name for the actual type + ClassificationTypeImpl classificationType = new ClassificationTypeImpl(type); + foreach (var baseType in baseTypes) + { + classificationType.AddBaseType(baseType); + } + + ClassificationTypes.Add(type, classificationType); + + return classificationType; + } + + /// <summary> + /// Create a transient classification type that can be used to represent + /// classification types generated at runtime. + /// </summary> + /// <param name="baseTypes">The base types associated with this transient type.</param> + /// <returns>The new transient type.</returns> + public IClassificationType CreateTransientClassificationType(IEnumerable<IClassificationType> baseTypes) + { + // Validate + if (baseTypes == null) + { + throw new ArgumentNullException("baseTypes"); + } + if (!baseTypes.GetEnumerator().MoveNext()) + { + throw new InvalidOperationException(LookUp.Strings.TransientTypesNeedAtLeastOneBaseType); + } + + return BuildTransientClassificationType(baseTypes); + } + + /// <summary> + /// Create a transient classification type that can be used to represent + /// classification types generated at runtime. + /// </summary> + /// <param name="baseTypes">The base types associated with this transient type.</param> + /// <returns>The new transient type.</returns> + public IClassificationType CreateTransientClassificationType(params IClassificationType[] baseTypes) + { + // Validate + if (baseTypes == null) + { + throw new ArgumentNullException("baseTypes"); + } + if (baseTypes.Length == 0) + { + throw new InvalidOperationException(LookUp.Strings.TransientTypesNeedAtLeastOneBaseType); + } + + return BuildTransientClassificationType(baseTypes); + } + #endregion // Public Members + + #region Private Methods + + /// <summary> + /// The transient type contributed by this assembly. + /// </summary> + private IClassificationType TransientClassificationType + { + get + { + return ClassificationTypes["(TRANSIENT)"]; + } + } + + /// <summary> + /// The map of classification type names to actual IClassificationTypes. + /// + /// Used to lazily init the map. + /// </summary> + private Dictionary<string, ClassificationTypeImpl> ClassificationTypes + { + get + { + if (_classificationTypes == null) + { + _classificationTypes = new Dictionary<string, ClassificationTypeImpl>(StringComparer.InvariantCultureIgnoreCase); + BuildClassificationTypes(_classificationTypes); + } + return _classificationTypes; + } + } + + /// <summary> + /// Consumes all of the IClassificationTypeProvisions in the system to build the + /// list of classification types in the system. + /// </summary> + private void BuildClassificationTypes(Dictionary<string,ClassificationTypeImpl> classificationTypes) + { + // For each content baseType provision, create an IClassificationType. + foreach (Lazy<ClassificationTypeDefinition, IClassificationTypeDefinitionMetadata> classificationTypeDefinition in _classificationTypeDefinitions) + { + string classificationName = classificationTypeDefinition.Metadata.Name; + + ClassificationTypeImpl type = null; + + if (!classificationTypes.TryGetValue(classificationName, out type)) + { + type = new ClassificationTypeImpl(classificationName); + classificationTypes.Add(classificationName, type); + } + + IEnumerable<string> baseTypes = classificationTypeDefinition.Metadata.BaseDefinition; + if (baseTypes != null) + { + ClassificationTypeImpl baseType = null; + + foreach (string baseClassificationType in baseTypes) + { + if (!classificationTypes.TryGetValue(baseClassificationType, out baseType)) + { + baseType = new ClassificationTypeImpl(baseClassificationType); + classificationTypes.Add(baseClassificationType, baseType); + } + + type.AddBaseType(baseType); + } + } + } + } + + /// <summary> + /// Builds a new transient classification type based on a set of actual base + /// types. + /// + /// With multiple projection buffers, it is possible to have a transient classification + /// type with transient types as parents. + /// </summary> + /// <param name="baseTypes"></param> + /// <returns></returns> + private IClassificationType BuildTransientClassificationType(IEnumerable<IClassificationType> baseTypes) + { + // Lazily init + if (_transientClassificationTypes == null) + { + _transientClassificationTypes = new Dictionary<string, ClassificationTypeImpl>(StringComparer.InvariantCultureIgnoreCase); + } + + List<IClassificationType> sortedBaseTypes = new List<IClassificationType>(baseTypes); + sortedBaseTypes.Sort(delegate(IClassificationType a, IClassificationType b) + { return string.CompareOrdinal(a.Classification, b.Classification); }); + + // Build the transient name + StringBuilder sb = new StringBuilder(); + foreach (IClassificationType type in sortedBaseTypes) + { + sb.Append(type.Classification); + sb.Append(" - "); + } + + // Append "(transient)" onto the name. + sb.Append(this.TransientClassificationType.Classification); + + // Look for a cached type + ClassificationTypeImpl transientType; + if (!_transientClassificationTypes.TryGetValue(sb.ToString(), out transientType)) + { + // Didn't find a cached type, so create a new one + transientType = new ClassificationTypeImpl(sb.ToString()); + + foreach (IClassificationType type in sortedBaseTypes) + { + transientType.AddBaseType(type); + } + + // Add in the transient type as a base type + transientType.AddBaseType(TransientClassificationType); + + // Cache this type so it doesn't need to be created again. + _transientClassificationTypes[transientType.Classification] = transientType; + } + + return transientType; + } + #endregion // Private Methods + } +} diff --git a/src/Text/Impl/ClassificationType/Strings.Designer.cs b/src/Text/Impl/ClassificationType/Strings.Designer.cs new file mode 100644 index 0000000..7ecc53b --- /dev/null +++ b/src/Text/Impl/ClassificationType/Strings.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:2.0.50727.1434 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.VisualStudio.Text.Classification.Implementation.LookUp { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Logic.Text.Classification.LookUp.Implementation.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to A classification with the same name has already been added.. + /// </summary> + internal static string ClassificationAlreadyAdded { + get { + return ResourceManager.GetString("ClassificationAlreadyAdded", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to A transient classification type needs at least one base type.. + /// </summary> + internal static string TransientTypesNeedAtLeastOneBaseType { + get { + return ResourceManager.GetString("TransientTypesNeedAtLeastOneBaseType", resourceCulture); + } + } + } +} diff --git a/src/Text/Impl/ClassificationType/Strings.resx b/src/Text/Impl/ClassificationType/Strings.resx new file mode 100644 index 0000000..5f64ec1 --- /dev/null +++ b/src/Text/Impl/ClassificationType/Strings.resx @@ -0,0 +1,126 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="ClassificationAlreadyAdded" xml:space="preserve"> + <value>A classification with the same name has already been added.</value> + </data> + <data name="TransientTypesNeedAtLeastOneBaseType" xml:space="preserve"> + <value>A transient classification type needs at least one base type.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/Text/Impl/DifferenceAlgorithm/CharacterDecompositionList.cs b/src/Text/Impl/DifferenceAlgorithm/CharacterDecompositionList.cs new file mode 100644 index 0000000..a4e0a78 --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/CharacterDecompositionList.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.Text.Differencing; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + /// <summary> + /// This is a decomposition of the given string into characters. + /// Note that this is still a DecomposedStringList, which is an IList<string>, + /// and so each string will be one character in length. + /// </summary> + internal class CharacterDecompositionList : ITokenizedStringListInternal + { + string _originalString; + SnapshotSpan _originalSpan; + + public CharacterDecompositionList(string original) + { + _originalString = original; + } + + public CharacterDecompositionList(SnapshotSpan original) + { + _originalSpan = original; + } + + public string Original + { + get + { + // A call to GetText() here could be very expensive in memory. Be careful! + return _originalString ?? _originalSpan.GetText(); + } + } + + public string OriginalSubstring(int start, int length) + { + if (_originalString != null) + return _originalString.Substring(start, length); + else + return _originalSpan.Snapshot.GetText(start + _originalSpan.Start.Position, length); + } + + public Span GetElementInOriginal(int index) + { + //If index == count, return a zero-length span at the end. + return new Span(index, (index < this.Count) ? 1 : 0); + } + + public Span GetSpanInOriginal(Span span) + { + return span; + } + + public int IndexOf(string item) + { + throw new NotSupportedException(); + } + + public void Insert(int index, string item) + { + throw new NotSupportedException(); + } + + public void RemoveAt(int index) + { + throw new NotSupportedException(); + } + + public string this[int index] + { + get + { + return this.OriginalSubstring(index, 1); + } + set + { + throw new NotSupportedException(); + } + } + + public void Add(string item) + { + throw new NotSupportedException(); + } + + public void Clear() + { + throw new NotSupportedException(); + } + + public bool Contains(string item) + { + throw new NotSupportedException(); + } + + public void CopyTo(string[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + public int Count + { + get { return _originalString != null ? _originalString.Length : _originalSpan.Length; } + } + + public bool IsReadOnly + { + get { return true; } + } + + public bool Remove(string item) + { + throw new NotSupportedException(); + } + + public IEnumerator<string> GetEnumerator() + { + for (int i = 0; i < Count; i++) + yield return this[i]; + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + } +} diff --git a/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs b/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs new file mode 100644 index 0000000..85f1156 --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/DefaultTextDifferencingService.cs @@ -0,0 +1,153 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ +// Ignore deprecated (IHierarchicalStringDifferenceService is deprecated) +#pragma warning disable 0618 + + [Export(typeof(IHierarchicalStringDifferenceService))] + internal sealed class DefaultTextDifferencingService : ITextDifferencingService, IHierarchicalStringDifferenceService + { + #region ITextDifferencingService-specific + + public IHierarchicalDifferenceCollection DiffSnapshotSpans(SnapshotSpan leftSpan, SnapshotSpan rightSpan, StringDifferenceOptions differenceOptions) + { + return this.DiffSnapshotSpans(leftSpan, rightSpan, differenceOptions, DefaultGetLineTextCallback); + } + + public IHierarchicalDifferenceCollection DiffSnapshotSpans(SnapshotSpan leftSpan, SnapshotSpan rightSpan, StringDifferenceOptions differenceOptions, Func<ITextSnapshotLine, string> getLineTextCallback) + { + StringDifferenceTypes type; + ITokenizedStringListInternal left; + ITokenizedStringListInternal right; + if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Line)) + { + type = StringDifferenceTypes.Line; + + left = new SnapshotLineList(leftSpan, getLineTextCallback, differenceOptions); + right = new SnapshotLineList(rightSpan, getLineTextCallback, differenceOptions); + } + else if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Word)) + { + type = StringDifferenceTypes.Word; + + left = new WordDecompositionList(leftSpan, differenceOptions); + right = new WordDecompositionList(rightSpan, differenceOptions); + } + else if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Character)) + { + type = StringDifferenceTypes.Character; + + left = new CharacterDecompositionList(leftSpan); + right = new CharacterDecompositionList(rightSpan); + } + else + { + throw new ArgumentOutOfRangeException("differenceOptions"); + } + + return DiffText(left, right, type, differenceOptions); + } + + public IHierarchicalDifferenceCollection DiffStrings(string leftString, string rightString, StringDifferenceOptions differenceOptions) + { + if (leftString == null) + throw new ArgumentNullException(nameof(leftString)); + if (rightString == null) + throw new ArgumentNullException(nameof(rightString)); + + StringDifferenceTypes type; + ITokenizedStringListInternal left; + ITokenizedStringListInternal right; + if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Line)) + { + type = StringDifferenceTypes.Line; + + left = new LineDecompositionList(leftString, differenceOptions.IgnoreTrimWhiteSpace); + right = new LineDecompositionList(rightString, differenceOptions.IgnoreTrimWhiteSpace); + } + else if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Word)) + { + type = StringDifferenceTypes.Word; + + left = new WordDecompositionList(leftString, differenceOptions); + right = new WordDecompositionList(rightString, differenceOptions); + } + else if (differenceOptions.DifferenceType.HasFlag(StringDifferenceTypes.Character)) + { + type = StringDifferenceTypes.Character; + + left = new CharacterDecompositionList(leftString); + right = new CharacterDecompositionList(rightString); + } + else + { + throw new ArgumentOutOfRangeException("differenceOptions"); + } + + return DiffText(left, right, type, differenceOptions); + } + + IHierarchicalDifferenceCollection DiffText(ITokenizedStringListInternal left, ITokenizedStringListInternal right, StringDifferenceTypes type, StringDifferenceOptions differenceOptions) + { + StringDifferenceOptions nextOptions = new StringDifferenceOptions(differenceOptions); + nextOptions.DifferenceType &= ~type; + + var diffCollection = ComputeMatches(type, differenceOptions, left, right); + return new HierarchicalDifferenceCollection(diffCollection, left, right, this, nextOptions); + } + + internal static List<Span> GetContiguousSpans(Span span, ITokenizedStringListInternal tokens) + { + List<Span> result = new List<Span>(); + int start = span.Start; + for (int i = span.Start + 1; (i < span.End); ++i) + { + if (tokens.GetElementInOriginal(i - 1).End != tokens.GetElementInOriginal(i).Start) + { + result.Add(Span.FromBounds(start, i)); + start = i; + } + } + + if (start < span.End) + { + result.Add(Span.FromBounds(start, span.End)); + } + + return result; + } + + internal static string DefaultGetLineTextCallback(ITextSnapshotLine line) + { + return line.GetTextIncludingLineBreak(); + } + + static IDifferenceCollection<string> ComputeMatches(StringDifferenceTypes differenceType, StringDifferenceOptions differenceOptions, + IList<string> leftSequence, IList<string> rightSequence) + { + return ComputeMatches(differenceType, differenceOptions, leftSequence, rightSequence, leftSequence, rightSequence); + } + + static IDifferenceCollection<string> ComputeMatches(StringDifferenceTypes differenceType, StringDifferenceOptions differenceOptions, + IList<string> leftSequence, IList<string> rightSequence, + IList<string> originalLeftSequence, IList<string> originalRightSequence) + { + return MaximalSubsequenceAlgorithm.DifferenceSequences(leftSequence, rightSequence, originalLeftSequence, originalRightSequence, differenceOptions.ContinueProcessingPredicate); + } + + #endregion + } + +#pragma warning restore 0618 +} diff --git a/src/Text/Impl/DifferenceAlgorithm/DiffChangeCollectionHelper.cs b/src/Text/Impl/DifferenceAlgorithm/DiffChangeCollectionHelper.cs new file mode 100644 index 0000000..1380f9f --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/DiffChangeCollectionHelper.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.Text.Differencing; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + internal static class DiffChangeCollectionHelper<T> + { + public static DifferenceCollection<T> Create(Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] changes, IList<T> originalLeft, IList<T> originalRight) + { + return new DifferenceCollection<T>(CreateDiffs(changes, originalLeft, originalRight), originalLeft, originalRight); + } + + static IList<Difference> CreateDiffs(Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] changes, IList<T> originalLeft, IList<T> originalRight) + { + IList<Difference> diffs = new List<Difference>(changes.Length); + + if (changes.Length != 0) + { + //Create the match before (if any) + var change = changes[0]; + Match before = DifferenceCollection<T>.CreateInitialMatch(change.OriginalStart); + + for (int i = 1; (i < changes.Length); ++i) + { + var nextChange = changes[i]; + + DifferenceCollection<T>.AddDifference(change.OriginalStart, change.OriginalEnd, nextChange.OriginalStart, + change.ModifiedStart, change.ModifiedEnd, nextChange.ModifiedStart, + diffs, ref before); + + change = nextChange; + } + + DifferenceCollection<T>.AddDifference(change.OriginalStart, change.OriginalEnd, originalLeft.Count, + change.ModifiedStart, change.ModifiedEnd, originalRight.Count, + diffs, ref before); + } + + return diffs; + } + } +} diff --git a/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs b/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs new file mode 100644 index 0000000..33d06cb --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/HierarchicalDifferenceCollection.cs @@ -0,0 +1,161 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + /// <summary> + /// The default implementation of IDecomposedDifferenceCollection. This maintains + /// a given IDifferenceCollection<string> and a mapping of Difference indices to + /// contained differences (if there are any). + /// </summary> + internal class HierarchicalDifferenceCollection : IHierarchicalDifferenceCollection + { + private readonly ITokenizedStringListInternal left; + private readonly ITokenizedStringListInternal right; + private readonly IDifferenceCollection<string> differenceCollection; + private readonly ITextDifferencingService differenceService; + private readonly StringDifferenceOptions options; + + private readonly ConcurrentDictionary<int, IHierarchicalDifferenceCollection> containedDifferences; + + /// <summary> + /// Create a new hierarchical difference collection. + /// </summary> + /// <param name="differenceCollection">The underlying difference collection for this level + /// of the hierarchy.</param> + /// <param name="differenceService">The difference service to use for doing the next level of + /// differencing</param> + /// <param name="options">The options to use for the next level of differencing. + /// If <see cref="StringDifferenceOptions.DifferenceType" /> is <c>0</c>, then + /// no further differencing will take place.</param> + public HierarchicalDifferenceCollection(IDifferenceCollection<string> differenceCollection, + ITokenizedStringListInternal left, + ITokenizedStringListInternal right, + ITextDifferencingService differenceService, + StringDifferenceOptions options) + { + if (differenceCollection == null) + throw new ArgumentNullException("differenceCollection"); + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + if (!object.ReferenceEquals(left, differenceCollection.LeftSequence)) + throw new ArgumentException("left must equal differenceCollection.LeftSequence"); + if (!object.ReferenceEquals(right, differenceCollection.RightSequence)) + throw new ArgumentException("right must equal differenceCollection.RightSequence"); + + this.left = left; + this.right = right; + + this.differenceCollection = differenceCollection; + this.differenceService = differenceService; + this.options = options; + + containedDifferences = new ConcurrentDictionary<int, IHierarchicalDifferenceCollection>(); + } + + #region IHierarchicalDifferenceCollection Members + + public ITokenizedStringList LeftDecomposition + { + get { return left; } + } + + public ITokenizedStringList RightDecomposition + { + get { return right; } + } + + public IHierarchicalDifferenceCollection GetContainedDifferences(int index) + { + if (options.DifferenceType == 0) + return null; + + return containedDifferences.GetOrAdd(index, CalculateContainedDiff); + } + + /// <summary> + /// Calculate the contained difference at the given index. Used by the concurrent dictionary's + /// GetOrAdd that takes a value factory. + /// </summary> + private IHierarchicalDifferenceCollection CalculateContainedDiff(int index) + { + // We need to compute the next level of differences. + var diff = this.Differences[index]; + + if (diff.DifferenceType == DifferenceType.Change) + { + Span leftSpan = this.left.GetSpanInOriginal(diff.Left); + Span rightSpan = this.right.GetSpanInOriginal(diff.Right); + + string leftString = this.left.OriginalSubstring(leftSpan.Start, leftSpan.Length); + string rightString = this.right.OriginalSubstring(rightSpan.Start, rightSpan.Length); + + return differenceService.DiffStrings(leftString, rightString, options); + } + + return null; + } + + public bool HasContainedDifferences(int index) + { + return GetContainedDifferences(index) != null; + } + + #endregion + + #region IDifferenceCollection<string> Members + + public IList<Difference> Differences + { + get { return differenceCollection.Differences; } + } + + public IList<string> LeftSequence + { + get { return left; } + } + + public IEnumerable<Tuple<int, int>> MatchSequence + { + get { return differenceCollection.MatchSequence; } + } + + public IList<string> RightSequence + { + get { return right; } + } + + #endregion + + #region IEnumerable<Difference> Members + + public IEnumerator<Difference> GetEnumerator() + { + return differenceCollection.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/src/Text/Impl/DifferenceAlgorithm/LineDecompositionList.cs b/src/Text/Impl/DifferenceAlgorithm/LineDecompositionList.cs new file mode 100644 index 0000000..7c27e7c --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/LineDecompositionList.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + // TODO: Replace the logic in this class with the upcoming Line/Word + // split utility. + + /// <summary> + /// This is a decomposition of the given string into lines. + /// </summary> + internal sealed class LineDecompositionList : TokenizedStringList + { + public LineDecompositionList(string original, bool ignoreTrimWhiteSpace) + : base(original) + { + if (original.Length == 0) + { + base.Tokens.Add(new Span(0, 0)); + } + else + { + int firstNonWhitespace = -1; + int lastNonWhitespace = -1; + int start = 0; + int i = 0; + while (i < original.Length) + { + int breakLength = TextUtilities.LengthOfLineBreak(original, i, original.Length); + if (breakLength > 0) + { + i += breakLength; + if (ignoreTrimWhiteSpace) + { + base.Tokens.Add((firstNonWhitespace == -1) + ? new Span(i - breakLength, 0) + : Span.FromBounds(firstNonWhitespace, lastNonWhitespace + 1)); + } + else + { + base.Tokens.Add(Span.FromBounds(start, i)); + } + + start = i; + + firstNonWhitespace = -1; + lastNonWhitespace = -1; + } + else + { + if (!char.IsWhiteSpace(original[i])) + { + if (firstNonWhitespace == -1) + { + firstNonWhitespace = i; + } + lastNonWhitespace = i; + } + + ++i; + } + } + + if (ignoreTrimWhiteSpace) + { + base.Tokens.Add((firstNonWhitespace == -1) + ? new Span(original.Length, 0) + : Span.FromBounds(firstNonWhitespace, lastNonWhitespace + 1)); + } + else + { + base.Tokens.Add(Span.FromBounds(start, original.Length)); + } + } + } + } +} diff --git a/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs b/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs new file mode 100644 index 0000000..a40aa72 --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/MaximalSubsequenceAlgorithm.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + /// <summary> + /// Generate a maximal common subsequence (or longest common subsequence) of + /// two sequences (ILists). + /// </summary> + [Export(typeof(IDifferenceService))] + internal sealed class MaximalSubsequenceAlgorithm : IDifferenceService + { + #region IDifferenceService Members + static readonly Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] Empty = new Microsoft.TeamFoundation.Diff.Copy.IDiffChange[0]; + + public IDifferenceCollection<T> DifferenceSequences<T>(IList<T> left, IList<T> right) + { + return DifferenceSequences<T>(left, right, null); + } + + public IDifferenceCollection<T> DifferenceSequences<T>(IList<T> left, IList<T> right, ContinueProcessingPredicate<T> continueProcessingPredicate) + { + return DifferenceSequences<T>(left, right, left, right, continueProcessingPredicate); + } + + #endregion + + internal static DifferenceCollection<T> DifferenceSequences<T>(IList<T> left, IList<T> right, IList<T> originalLeft, IList<T> originalRight, ContinueProcessingPredicate<T> continueProcessingPredicate) + { + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + + Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] changes; + if ((left.Count == 0) || (right.Count == 0)) + { + if ((left.Count == 0) && (right.Count == 0)) + { + changes = MaximalSubsequenceAlgorithm.Empty; + } + else + { + changes = new Microsoft.TeamFoundation.Diff.Copy.IDiffChange[1]; + changes[0] = new Microsoft.TeamFoundation.Diff.Copy.DiffChange(0, left.Count, + 0, right.Count); + } + } + else + changes = ComputeMaximalSubsequence<T>(left, right, continueProcessingPredicate); + + return DiffChangeCollectionHelper<T>.Create(changes, originalLeft, originalRight); + } + + private static Microsoft.TeamFoundation.Diff.Copy.IDiffChange[] ComputeMaximalSubsequence<T>(IList<T> left, IList<T> right, ContinueProcessingPredicate<T> continueProcessingPredicate) + { + var lcs = new Microsoft.TeamFoundation.Diff.Copy.LcsDiff<T>(); + var diffs = lcs.Diff(left, right, EqualityComparer<T>.Default, (continueProcessingPredicate == null) + ? (Microsoft.TeamFoundation.Diff.Copy.ContinueDifferencePredicate<T>)null + : (int originalIndex, IList<T> originalSequence, int longestMatchSoFar) => { return continueProcessingPredicate(originalIndex, originalSequence, longestMatchSoFar);}); + + return diffs; + } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs b/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs new file mode 100644 index 0000000..47df212 --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/SnapshotLineList.cs @@ -0,0 +1,214 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + /// <summary> + /// This class translates an <see cref="ITextSnapshot"/> and a set of line transforms + /// into a list of lines that is suitable for diffing. The line transforms are evaluated + /// as each line is requested, to minimize the extra memory required to translate every + /// line of the snapshot. + /// </summary> + class SnapshotLineList : IList<string>, ITokenizedStringListInternal + { + SnapshotSpan _snapshotSpan; + Span _lineSpan; + Func<ITextSnapshotLine, string> _getLineTextCallback; + StringDifferenceOptions _options; + + public SnapshotLineList(SnapshotSpan snapshotSpan, Func<ITextSnapshotLine, string> getLineTextCallback, StringDifferenceOptions options) + { + if (getLineTextCallback == null) + throw new ArgumentNullException("getLineTextCallback"); + if ((options.DifferenceType & StringDifferenceTypes.Line) == 0) + throw new InvalidOperationException("This collection can only be used for line differencing"); + + _snapshotSpan = snapshotSpan; + _getLineTextCallback = getLineTextCallback; + _options = options; + + // Figure out the first and last line in the span + var startLine = snapshotSpan.Start.GetContainingLine(); + int start = snapshotSpan.Start.GetContainingLine().LineNumber; + + //Perf hack to avoid calling GetContainingLine() if the lines are the same. + SnapshotPoint endPoint = snapshotSpan.End; + int end = ((endPoint.Position < startLine.EndIncludingLineBreak) ? start : endPoint.GetContainingLine().LineNumber) + 1; + + _lineSpan = Span.FromBounds(start, end); + } + + public int Count + { + get { return _lineSpan.Length; } + } + + public string this[int index] + { + get + { + SnapshotSpan lineSpan = GetSpanOfIndex(index); + ITextSnapshotLine line = _snapshotSpan.Snapshot.GetLineFromLineNumber(_lineSpan.Start + index); + + bool isPartialLine = lineSpan.Length != line.LengthIncludingLineBreak; + + string text; + if (isPartialLine) + text = lineSpan.GetText(); + else + text = _getLineTextCallback(line); + + if (_options.IgnoreTrimWhiteSpace) + { + if (isPartialLine) + { + // For a partial line, only trim the sides that are included in the line. + // This may not be entirely exact (the partial line may still include what would have been + // leading whitespace), but we're ok with it for partial lines, which already don't use the + // provided _getLineTextCallback. + if (lineSpan.Start == line.Start) + text = text.TrimStart(); + if (lineSpan.End == line.EndIncludingLineBreak) + text = text.TrimEnd(); + } + else + { + text = text.Trim(); + } + } + + return text; + } + set + { + throw new NotSupportedException(); + } + } + + SnapshotSpan GetSpanOfIndex(int index) + { + if (index < 0 || index >= _lineSpan.Length) + throw new ArgumentOutOfRangeException("index"); + + ITextSnapshotLine line = _snapshotSpan.Snapshot.GetLineFromLineNumber(_lineSpan.Start + index); + SnapshotSpan? lineSpan = line.ExtentIncludingLineBreak.Intersection(_snapshotSpan); + if (!lineSpan.HasValue) + { + Debug.Fail("Unexpected - we have a line with no intersection."); + return new SnapshotSpan(line.Start, 0); + } + + return lineSpan.Value; + } + + #region Not supported + + public int IndexOf(string item) + { + throw new NotSupportedException(); + } + + public void Insert(int index, string item) + { + throw new NotSupportedException(); + } + + public void RemoveAt(int index) + { + throw new NotSupportedException(); + } + + public void Add(string item) + { + throw new NotSupportedException(); + } + + public void Clear() + { + throw new NotSupportedException(); + } + + public bool Contains(string item) + { + throw new NotSupportedException(); + } + + public void CopyTo(string[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + public bool IsReadOnly + { + get { return true; } + } + + public bool Remove(string item) + { + throw new NotSupportedException(); + } + + public IEnumerator<string> GetEnumerator() + { + for (int i = 0; i < Count ; i++) + { + yield return this[i]; + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return ((IEnumerable<string>)this).GetEnumerator(); + } + + #endregion + + public string Original + { + get + { + return _snapshotSpan.GetText(); + } + } + + public string OriginalSubstring(int startIndex, int length) + { + return _snapshotSpan.Snapshot.GetText(_snapshotSpan.Start + startIndex, length); + } + + public Span GetElementInOriginal(int index) + { + if (index == _lineSpan.Length) + { + return new Span(_snapshotSpan.End, 0); + } + else + { + // Get the span for the index, but make sure to offset by the _snapshotSpan, + // so the coordinates are relative to Original (and not the snapshot itself) + SnapshotSpan span = GetSpanOfIndex(index); + return new Span(span.Start - _snapshotSpan.Start, span.Length); + } + } + + public Span GetSpanInOriginal(Span span) + { + int startPoint = GetElementInOriginal(span.Start).Start; + + if (span.IsEmpty) + return new Span(startPoint, 0); + + int endPoint = GetElementInOriginal(span.End - 1).End; + + return Span.FromBounds(startPoint, endPoint); + } + } +} diff --git a/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs b/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs new file mode 100644 index 0000000..6e2bfd3 --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/TFS/DiffFinder.cs @@ -0,0 +1,974 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Text; + +//************************************************************************* +// The code from this point on is a soure-port of the TFS diff algorithm, to be available +// in cases where we don't have access to the TFS diff assembly (i.e. outside of Visual Studio). +//************************************************************************* + +namespace Microsoft.TeamFoundation.Diff.Copy +{ + //************************************************************************* + /// <summary> + /// A predicate used by Microsoft.TeamFoundation.Diff.DiffFinder + /// that allows callers to stop differencing prematurely. + /// </summary> + /// <param name="originalIndex">The current index in the original sequence being differenced.</param> + /// <param name="originalSequence">The original sequence being differenced.</param> + /// <param name="longestMatchSoFar">The length of the longest match so far.</param> + /// <returns>true if the algorithm should continue processing, false to stop the algorithm.</returns> + /// <remarks> + /// When false is returned, the algorithm stops searching for matches and uses + /// the information it has computed so far to create the Microsoft.TeamFoundation.Diff.IDiffChange[] array + /// that will be returned. + /// </remarks> + //************************************************************************* + public delegate bool ContinueDifferencePredicate<T>(int originalIndex, + IList<T> originalSequence, int longestMatchSoFar); + + //************************************************************************* + /// <summary> + /// An enumeration of the possible change types for a difference operation. + /// </summary> + //************************************************************************* + public enum DiffChangeType + { + //********************************************************************* + /// <summary> + /// Content was inserted into the modified sequence. + /// </summary> + //********************************************************************* + Insert, + + //********************************************************************* + /// <summary> + /// Content was deleted from the original sequence. + /// </summary> + //********************************************************************* + Delete, + + //********************************************************************* + /// <summary> + /// Content from the original sequence has changed in the modified + /// sequence. + /// </summary> + //********************************************************************* + Change + } + + //************************************************************************* + /// <summary> + /// Represents information about a specific difference between two sequences. + /// </summary> + //************************************************************************* + public interface IDiffChange + { + //********************************************************************* + /// <summary> + /// The type of difference. + /// </summary> + //********************************************************************* + DiffChangeType ChangeType { get; } + + //********************************************************************* + /// <summary> + /// The position of the first element in the original sequence which + /// this change affects. + /// </summary> + //********************************************************************* + int OriginalStart { get; } + + //********************************************************************* + /// <summary> + /// The number of elements from the original sequence which were + /// affected (deleted). + /// </summary> + //********************************************************************* + int OriginalLength { get; } + + //********************************************************************* + /// <summary> + /// The position of the last element in the original sequence which + /// this change affects. + /// </summary> + //********************************************************************* + int OriginalEnd { get; } + + //********************************************************************* + /// <summary> + /// The position of the first element in the modified sequence which + /// this change affects. + /// </summary> + //********************************************************************* + int ModifiedStart { get; } + + //********************************************************************* + /// <summary> + /// The number of elements from the modified sequence which were + /// affected (added). + /// </summary> + //********************************************************************* + int ModifiedLength { get; } + + //********************************************************************* + /// <summary> + /// The position of the last element in the modified sequence which + /// this change affects. + /// </summary> + //********************************************************************* + int ModifiedEnd { get; } + + /// <summary> + /// This methods combines two IDiffChange objects into one + /// </summary> + /// <param name="diffChange">The diff change to add</param> + /// <returns>An IDiffChange that represnets this + diffChange</returns> + IDiffChange Add(IDiffChange diffChange); + } + + //************************************************************************* + /// <summary> + /// Represents information about a specific difference between two sequences. + /// </summary> + //************************************************************************* + internal class DiffChange : IDiffChange + { + //********************************************************************* + /// <summary> + /// Constructs a new DiffChange with the given sequence information + /// and content. + /// </summary> + /// <param name="originalStart">The start position of the difference + /// in the original sequence.</param> + /// <param name="originalLength">The number of elements of the difference + /// from the original sequence.</param> + /// <param name="modifiedStart">The start position of the difference + /// in the modified sequence.</param> + /// <param name="modifiedLength">The number of elements of the difference + /// from the modified sequence.</param> + //********************************************************************* + internal DiffChange(int originalStart, int originalLength, int modifiedStart, int modifiedLength) + { + Debug.Assert(originalLength > 0 || modifiedLength > 0, "originalLength and modifiedLength cannot both be <= 0"); + + m_originalStart = originalStart; + m_originalLength = originalLength; + m_modifiedStart = modifiedStart; + m_modifiedLength = modifiedLength; + UpdateChangeType(); + } + + //********************************************************************* + /// <summary> + /// Determines the change type from the ranges and updates the value. + /// </summary> + //********************************************************************* + private void UpdateChangeType() + { + // Figure out what change type this is + if (m_originalLength > 0) + { + if (m_modifiedLength > 0) + { + m_changeType = DiffChangeType.Change; + } + else + { + m_changeType = DiffChangeType.Delete; + } + } + else if (m_modifiedLength > 0) + { + m_changeType = DiffChangeType.Insert; + } + m_updateChangeType = false; + } + + //********************************************************************* + /// <summary> + /// The type of difference. + /// </summary> + //********************************************************************* + public DiffChangeType ChangeType + { + get + { + if (m_updateChangeType) + { + UpdateChangeType(); + } + + return m_changeType; + } + } + private DiffChangeType m_changeType; + + //********************************************************************* + /// <summary> + /// The position of the first element in the original sequence which + /// this change affects. + /// </summary> + //********************************************************************* + public int OriginalStart + { + get { return m_originalStart; } + set + { + m_originalStart = value; + m_updateChangeType = true; + } + } + private int m_originalStart; + + //********************************************************************* + /// <summary> + /// The number of elements from the original sequence which were + /// affected. + /// </summary> + //********************************************************************* + public int OriginalLength + { + get { return m_originalLength; } + set + { + m_originalLength = value; + m_updateChangeType = true; + } + } + private int m_originalLength; + + //********************************************************************* + /// <summary> + /// The position of the last element in the original sequence which + /// this change affects. + /// </summary> + //********************************************************************* + public int OriginalEnd + { + get { return OriginalStart + OriginalLength; } + } + + //********************************************************************* + /// <summary> + /// The position of the first element in the modified sequence which + /// this change affects. + /// </summary> + //********************************************************************* + public int ModifiedStart + { + get { return m_modifiedStart; } + set + { + m_modifiedStart = value; + m_updateChangeType = true; + } + } + private int m_modifiedStart; + + //********************************************************************* + /// <summary> + /// The number of elements from the modified sequence which were + /// affected (added). + /// </summary> + //********************************************************************* + public int ModifiedLength + { + get { return m_modifiedLength; } + set + { + m_modifiedLength = value; + m_updateChangeType = true; + } + } + private int m_modifiedLength; + + //********************************************************************* + /// <summary> + /// The position of the last element in the modified sequence which + /// this change affects. + /// </summary> + //********************************************************************* + public int ModifiedEnd + { + get { return ModifiedStart + ModifiedLength; } + } + + /// <summary> + /// This methods combines two DiffChange objects into one + /// </summary> + /// <param name="diffChange">The diff change to add</param> + /// <returns>A IDiffChange object that represnets this + diffChange</returns> + public IDiffChange Add(IDiffChange diffChange) + { + //If the diff change is null then just return this + if (diffChange == null) + { + return this; + } + + int originalStart = Math.Min(this.OriginalStart, diffChange.OriginalStart); + int originalEnd = Math.Max(this.OriginalEnd, diffChange.OriginalEnd); + + int modifiedStart = Math.Min(this.ModifiedStart, diffChange.ModifiedStart); + int modifiedEnd = Math.Max(this.ModifiedEnd, diffChange.ModifiedEnd); + + return new DiffChange(originalStart, originalEnd - originalStart, modifiedStart, modifiedEnd - modifiedStart); + } + + // Member variables + private bool m_updateChangeType; + } + + //************************************************************************* + /// <summary> + /// The types of End of Line terminators. + /// </summary> + //************************************************************************* + public enum EndOfLineTerminator + { + None, // EndOfFile + LineFeed, // \n (UNIX) + CarriageReturn, // \r (Mac) + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "LineFeed")] + [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "LineFeed")] + CarriageReturnLineFeed, // \r\n (DOS) + LineSeparator, // \u2028 (Unicode character) + ParagraphSeparator, // \u2029 (Unicode character) + NextLine // \u0085 (Unicode character) + } + + //************************************************************************* + /// <summary> + /// Utility class which tokenizes the given stream into string tokens. Each + /// token represents a line from the file as delimited by common EOL sequences. + /// (\n, \r\n, \r, \u2028, \u2029, \u85) + /// </summary> + //************************************************************************* + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Tokenizer")] + public class DiffLineTokenizer + { + //********************************************************************* + /// <summary> + /// Constructs a new DiffLineTokenizer for the given stream and encoding. + /// </summary> + /// <param name="stream">The stream to tokenize.</param> + /// <param name="encoding">The character encoding of the bytes + /// in the stream. If null, this will be automatically detected.</param> + //********************************************************************* + public DiffLineTokenizer(Stream stream, Encoding encoding) + { + if (encoding == null) + { + m_streamReader = new StreamReader(stream, true); + } + else + { + m_streamReader = new StreamReader(stream, encoding, true); + } + + // Bug 70126: Diff calculated code churn incorrectly for file + // We don't want to parse a Unicode EOL character if its not Unicode + int codePage = m_streamReader.CurrentEncoding.CodePage; + if (codePage == Encoding.Unicode.CodePage || codePage == Encoding.BigEndianUnicode.CodePage || + codePage == Encoding.UTF32.CodePage || codePage == Encoding.UTF7.CodePage || + codePage == Encoding.UTF8.CodePage) + { + m_isUnicodeEncoding = true; + } + + // StreamReader has a perf optimization if we ask for at least as many chars + // as the internal byte buffer could produce if it were fully filled. + // Their internal byte buffer is 1024 bytes. + m_bufferSize = m_streamReader.CurrentEncoding.GetMaxCharCount(1024); + m_charBuffer = new char[m_bufferSize]; + m_stringBuilder = new StringBuilder(80); + } + + //********************************************************************* + /// <summary> + /// Fills up the internal buffer with characters from the stream. + /// Returns the number of characters in the buffer and resets the + /// current buffer position. + /// Returns 0 when EOF. + /// </summary> + //********************************************************************* + private int FillBuffer() + { + m_currBufferPos = 0; + m_numCharsInBuffer = m_streamReader.Read(m_charBuffer, 0, m_bufferSize); + return m_numCharsInBuffer; + } + + //********************************************************************* + /// <summary> + /// Gets the next line token as a string from the stream. + /// </summary> + /// <returns>The next token. Returns null when the end of stream has + /// been reached.</returns> + //********************************************************************* + public string NextLineToken(out EndOfLineTerminator endOfLine) + { + // The structure of this code is borrowed from StreamReader.ReadLine() + // Except that it has been extended out to support Unicode EOL characters. + m_stringBuilder.Length = 0; + endOfLine = EndOfLineTerminator.None; + + if (m_currBufferPos == m_numCharsInBuffer && FillBuffer() == 0) + { + // If the buffer was empty, and we couldn't refill it, we're done. + return null; + } + do + { + int index = m_currBufferPos; + do + { + char ch = m_charBuffer[index]; + if (ch == '\r' || ch == '\n' || + (m_isUnicodeEncoding && (ch == LineSeparator || ch == ParagraphSeparator || ch == NextLine))) + { + switch ((int)ch) + { + case '\r': + endOfLine = EndOfLineTerminator.CarriageReturn; + break; + case '\n': + endOfLine = EndOfLineTerminator.LineFeed; + break; + case LineSeparator: + endOfLine = EndOfLineTerminator.LineSeparator; + break; + case ParagraphSeparator: + endOfLine = EndOfLineTerminator.ParagraphSeparator; + break; + case NextLine: + endOfLine = EndOfLineTerminator.NextLine; + break; + } + + String line; + if (m_stringBuilder.Length > 0) + { + m_stringBuilder.Append(m_charBuffer, m_currBufferPos, index - m_currBufferPos); + line = m_stringBuilder.ToString(); + } + else + { + line = new String(m_charBuffer, m_currBufferPos, index - m_currBufferPos); + } + + m_currBufferPos = index + 1; + if (ch == '\r' && (m_currBufferPos < m_numCharsInBuffer || FillBuffer() > 0)) + { + if (m_charBuffer[m_currBufferPos] == '\n') + { + endOfLine = EndOfLineTerminator.CarriageReturnLineFeed; + m_currBufferPos++; + } + } + + return line; + } + index++; + } while (index < m_numCharsInBuffer); + m_stringBuilder.Append(m_charBuffer, m_currBufferPos, m_numCharsInBuffer - m_currBufferPos); + } while (FillBuffer() > 0); + + return m_stringBuilder.ToString(); + } + + // Misc Unicode EOL characters. + private const int LineSeparator = 0x2028; + private const int ParagraphSeparator = 0x2029; + private const int NextLine = 0x85; + + // Member variables + private bool m_isUnicodeEncoding; + private int m_bufferSize; + private int m_numCharsInBuffer; + private int m_currBufferPos; + private char[] m_charBuffer; + private StringBuilder m_stringBuilder; + private StreamReader m_streamReader; + } + + //************************************************************************* + /// <summary> + /// A utility class which helps to create the set of DiffChanges from + /// a difference operation. This class accepts original DiffElements and + /// modified DiffElements that are involved in a particular change. The + /// MarktNextChange() method can be called to mark the seration between + /// distinct changes. At the end, the Changes property can be called to retrieve + /// the constructed changes. + /// </summary> + //************************************************************************* + internal class DiffChangeHelper : IDisposable + { + //********************************************************************* + /// <summary> + /// Constructs a new DiffChangeHelper for the given DiffSequences. + /// </summary> + /// <param name="originalSequence">The original sequence.</param> + /// <param name="modifiedSequnece">The modified sequence.</param> + //********************************************************************* + public DiffChangeHelper() + { + m_changes = new List<IDiffChange>(); + m_originalStart = Int32.MaxValue; + m_modifiedStart = Int32.MaxValue; + } + + //********************************************************************* + /// <summary> + /// Disposes of the resources used by this helper class. + /// </summary> + //********************************************************************* + public void Dispose() + { + if (m_changes != null) + { + m_changes = null; + } + GC.SuppressFinalize(this); + } + + //********************************************************************* + /// <summary> + /// Marks the beginning of the next change in the set of differences. + /// </summary> + //********************************************************************* + public void MarkNextChange() + { + // Only add to the list if there is something to add + if (m_originalCount > 0 || m_modifiedCount > 0) + { + // Add the new change to our list + m_changes.Add(new DiffChange(m_originalStart, m_originalCount, + m_modifiedStart, m_modifiedCount)); + } + + // Reset for the next change + m_originalCount = 0; + m_modifiedCount = 0; + m_originalStart = Int32.MaxValue; + m_modifiedStart = Int32.MaxValue; + } + + //********************************************************************* + /// <summary> + /// Adds the original element at the given position to the elements + /// affected by the current change. The modified index gives context + /// to the change position with respect to the original sequence. + /// </summary> + /// <param name="originalIndex">The index of the original element to add.</param> + /// <param name="modifiedIndex">The index of the modified element that + /// provides corresponding position in the modified sequence.</param> + //********************************************************************* + public void AddOriginalElement(int originalIndex, int modifiedIndex) + { + // The 'true' start index is the smallest of the ones we've seen + m_originalStart = Math.Min(m_originalStart, originalIndex); + m_modifiedStart = Math.Min(m_modifiedStart, modifiedIndex); + + m_originalCount++; + } + + //********************************************************************* + /// <summary> + /// Adds the modified element at the given position to the elements + /// affected by the current change. The original index gives context + /// to the change position with respect to the modified sequence. + /// </summary> + /// <param name="originalIndex">The index of the original element that + /// provides corresponding position in the original sequence.</param> + /// <param name="modifiedIndex">The index of the modified element to add.</param> + //********************************************************************* + public void AddModifiedElement(int originalIndex, int modifiedIndex) + { + // The 'true' start index is the smallest of the ones we've seen + m_originalStart = Math.Min(m_originalStart, originalIndex); + m_modifiedStart = Math.Min(m_modifiedStart, modifiedIndex); + + m_modifiedCount++; + } + + //********************************************************************* + /// <summary> + /// Retrieves all of the changes marked by the class. + /// </summary> + //********************************************************************* + public IDiffChange[] Changes + { + get + { + if (m_originalCount > 0 || m_modifiedCount > 0) + { + // Finish up on whatever is left + MarkNextChange(); + } + + return m_changes.ToArray(); + } + } + + //********************************************************************* + /// <summary> + /// Retrieves all of the changes marked by the class in the reverse order + /// </summary> + //********************************************************************* + public IDiffChange[] ReverseChanges + { + get + { + if (m_originalCount > 0 || m_modifiedCount > 0) + { + // Finish up on whatever is left + MarkNextChange(); + } + + m_changes.Reverse(); + return m_changes.ToArray(); + } + } + + // Member variables + private int m_originalCount; + private int m_modifiedCount; + private List<IDiffChange> m_changes; + private int m_originalStart; + private int m_modifiedStart; + } + + //************************************************************************* + /// <summary> + /// A base for classes which compute the differences between two input sequences. + /// </summary> + //************************************************************************* + public abstract class DiffFinder<T> : IDisposable + { + //************************************************************************* + /// <summary> + /// The original sequence + /// </summary> + //************************************************************************* + protected IList<T> OriginalSequence + { + get { return m_original; } + } + + //************************************************************************* + /// <summary> + /// The modified sequence + /// </summary> + //************************************************************************* + protected IList<T> ModifiedSequence + { + get { return m_modified; } + } + + //************************************************************************* + /// <summary> + /// The element comparer + /// </summary> + //************************************************************************* + protected IEqualityComparer<T> ElementComparer + { + get { return m_elementComparer; } + } + + //************************************************************************* + /// <summary> + /// Disposes resources used by this DiffFinder + /// </summary> + //************************************************************************* + public virtual void Dispose() + { + if (m_originalIds != null) + { + m_originalIds = null; + } + if (m_modifiedIds != null) + { + m_modifiedIds = null; + } + GC.SuppressFinalize(this); + } + + //********************************************************************* + /// <summary> + /// Returns true if the specified original and modified elements are equal. + /// </summary> + /// <param name="originalIndex">The index of the original element</param> + /// <param name="modifiedIndex">The index of the modified element</param> + /// <returns>True if the specified elements are equal</returns> + //********************************************************************* + protected bool ElementsAreEqual(int originalIndex, int modifiedIndex) + { + return ElementsAreEqual(originalIndex, true, modifiedIndex, false); + } + + //********************************************************************* + /// <summary> + /// Returns true if the two specified original elements are equal. + /// </summary> + /// <param name="firstIndex">The index of the first original element</param> + /// <param name="secondIndex">The index of the second original element</param> + /// <returns>True if the specified elements are equal</returns> + //********************************************************************* + protected bool OriginalElementsAreEqual(int firstIndex, int secondIndex) + { + return ElementsAreEqual(firstIndex, true, secondIndex, true); + } + + //********************************************************************* + /// <summary> + /// Returns true if the two specified modified elements are equal. + /// </summary> + /// <param name="firstIndex">The index of the first modified element</param> + /// <param name="secondIndex">The index of the second modified element</param> + /// <returns>True if the specified elements are equal</returns> + //********************************************************************* + protected bool ModifiedElementsAreEqual(int firstIndex, int secondIndex) + { + return ElementsAreEqual(firstIndex, false, secondIndex, false); + } + + //********************************************************************* + /// <summary> + /// Returns true if the specified elements are equal. + /// </summary> + /// <param name="firstIndex">The index of the first element</param> + /// <param name="firstIsOriginal">True if the first element is an original + /// element, false if modified element</param> + /// <param name="secondIndex">The index of the second element</param> + /// <param name="secondIsOriginal">True if the second element is an original + /// element, false if modified element</param> + /// <returns>True if the specified elements are equal</returns> + //********************************************************************* + private bool ElementsAreEqual(int firstIndex, bool firstIsOriginal, + int secondIndex, bool secondIsOriginal) + { + int firstId = firstIsOriginal ? m_originalIds[firstIndex] : m_modifiedIds[firstIndex]; + int secondId = secondIsOriginal ? m_originalIds[secondIndex] : m_modifiedIds[secondIndex]; + if (firstId != 0 && secondId != 0) + { + return firstId == secondId; + } + else + { + T firstElement = firstIsOriginal ? OriginalSequence[firstIndex] : ModifiedSequence[firstIndex]; + T secondElement = secondIsOriginal ? OriginalSequence[secondIndex] : ModifiedSequence[secondIndex]; + return ElementComparer.Equals(firstElement, secondElement); + } + } + + //************************************************************************* + /// <summary> + /// This method hashes element groups within the specified range + /// and assigns unique identifiers to identical elements. + /// + /// This greatly speeds up element comparison as we may compare integers + /// instead of having to look at element content. + /// </summary> + //************************************************************************* + private void ComputeUniqueIdentifiers(int originalStart, int originalEnd, + int modifiedStart, int modifiedEnd) + { + Debug.Assert(originalStart >= 0 && originalEnd < OriginalSequence.Count, "Original range is invalid"); + Debug.Assert(modifiedStart >= 0 && modifiedEnd < ModifiedSequence.Count, "Modified range is invalid"); + + // Create a new hash table for unique elements from the original + // sequence. + Dictionary<T, int> hashTable = new Dictionary<T, int>(OriginalSequence.Count + ModifiedSequence.Count, + ElementComparer); + int currentUniqueId = 1; + + // Fill up the hash table for unique elements + for (int i = originalStart; i <= originalEnd; i++) + { + T originalElement = OriginalSequence[i]; + if (!hashTable.TryGetValue(originalElement, out m_originalIds[i])) + { + // No entry in the hashtable so this is a new unique element. + // Assign the element a new unique identifier and add it to the + // hash table + m_originalIds[i] = currentUniqueId++; + hashTable.Add(originalElement, m_originalIds[i]); + } + } + + // Now match up modified elements + for (int i = modifiedStart; i <= modifiedEnd; i++) + { + T modifiedElement = ModifiedSequence[i]; + if (!hashTable.TryGetValue(modifiedElement, out m_modifiedIds[i])) + { + m_modifiedIds[i] = currentUniqueId++; + hashTable.Add(modifiedElement, m_modifiedIds[i]); + } + } + } + + //************************************************************************* + /// <summary> + /// Computes the differences between the given original and modified sequences. + /// </summary> + /// <param name="original">The original sequence.</param> + /// <param name="modified">The modified sequence.</param> + /// <param name="elementComparer">The diff element comparer.</param> + /// <returns>The set of differences between the sequences.</returns> + //************************************************************************* + public IDiffChange[] Diff(IList<T> original, IList<T> modified, IEqualityComparer<T> elementComparer) + { + return Diff(original, modified, elementComparer, null); + } + + //************************************************************************* + /// <summary> + /// Computes the differences between the given original and modified sequences. + /// </summary> + /// <param name="original">The original sequence.</param> + /// <param name="modified">The modified sequence.</param> + /// <param name="elementComparer">The diff element comparer.</param> + /// <returns>The set of differences between the sequences.</returns> + //************************************************************************* + public IDiffChange[] Diff(IList<T> original, IList<T> modified, IEqualityComparer<T> elementComparer, + ContinueDifferencePredicate<T> predicate) + { + Debug.Assert(original != null, "original is null"); + Debug.Assert(modified != null, "modified is null"); + Debug.Assert(elementComparer != null, "elementComparer is null"); + + m_original = original; + m_modified = modified; + m_elementComparer = elementComparer; + m_predicate = predicate; + + m_originalIds = new int[OriginalSequence.Count]; + m_modifiedIds = new int[ModifiedSequence.Count]; + + int originalStart = 0; + int originalEnd = OriginalSequence.Count - 1; + int modifiedStart = 0; + int modifiedEnd = ModifiedSequence.Count - 1; + + // Find the start of the differences + while (originalStart <= originalEnd && modifiedStart <= modifiedEnd && + ElementsAreEqual(originalStart, modifiedStart)) + { + originalStart++; + modifiedStart++; + } + + // Find the end of the differences + while (originalEnd >= originalStart && modifiedEnd >= modifiedStart && + ElementsAreEqual(originalEnd, modifiedEnd)) + { + originalEnd--; + modifiedEnd--; + } + + // In the very special case where one of the ranges is negative + // Either the sequences are identical, or there is exactly one insertion + // or there is exactly one deletion. We have no need to compute the diffs. + if (originalStart > originalEnd || modifiedStart > modifiedEnd) + { + IDiffChange[] changes; + + if (modifiedStart <= modifiedEnd) + { + Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd"); + + // All insertions + changes = new IDiffChange[1]; + changes[0] = new DiffChange(originalStart, 0, + modifiedStart, + modifiedEnd - modifiedStart + 1); + } + else if (originalStart <= originalEnd) + { + Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); + + // All deletions + changes = new IDiffChange[1]; + changes[0] = new DiffChange(originalStart, + originalEnd - originalStart + 1, + modifiedStart, 0); + } + else + { + Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd"); + Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); + + // Identical sequences - No differences + changes = new IDiffChange[0]; + } + + return changes; + } + + // Now that we have our bounds, calculate unique ids for all + // IDiffElements in the bounded range. That way, we speed up + // element comparisons during the diff computation. + ComputeUniqueIdentifiers(originalStart, originalEnd, + modifiedStart, modifiedEnd); + + // Compute the diffences on the bounded range + return ComputeDiff(originalStart, originalEnd, + modifiedStart, modifiedEnd); + } + + //************************************************************************* + /// <summary> + /// Computes the differences between the original and modified input + /// sequences on the bounded range. + /// </summary> + /// <returns>An array of the differences between the two input + /// sequences.</returns> + //************************************************************************* + protected abstract IDiffChange[] ComputeDiff(int originalStart, int originalEnd, + int modifiedStart, int modifiedEnd); + + //************************************************************************* + /// <summary> + /// Gets the LCS Diff implementation. + /// </summary> + //************************************************************************* + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Lcs")] + public static DiffFinder<T> LcsDiff + { + get { return new LcsDiff<T>(); } + } + + protected ContinueDifferencePredicate<T> ContinueDifferencePredicate + { + get + { + return m_predicate; + } + } + + // Member variables + private IList<T> m_original; + private IList<T> m_modified; + private int[] m_originalIds; + private int[] m_modifiedIds; + private IEqualityComparer<T> m_elementComparer; + + //Early termination predicate + private ContinueDifferencePredicate<T> m_predicate; + } +}
\ No newline at end of file diff --git a/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs b/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs new file mode 100644 index 0000000..c70ca9d --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/TFS/LCSDiff.cs @@ -0,0 +1,801 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; + +//************************************************************************* +// The code from this point on is a soure-port of the TFS diff algorithm, to be available +// in cases where we don't have access to the TFS diff assembly (i.e. outside of Visual Studio). +//************************************************************************* + +namespace Microsoft.TeamFoundation.Diff.Copy +{ + //************************************************************************* + /// <summary> + /// An implementation of the difference algorithm described in + /// "An O(ND) Difference Algorithm and its Variations" by Eugene W. Myers + /// </summary> + //************************************************************************* + internal class LcsDiff<T> : DiffFinder<T> + { + //************************************************************************* + /// <summary> + /// Constructs the DiffFinder + /// </summary> + //************************************************************************* + public LcsDiff() + : base() + { + m_forwardHistory = new List<int[]>(MaxDifferencesHistory); + m_reverseHistory = new List<int[]>(MaxDifferencesHistory); + } + + //************************************************************************* + /// <summary> + /// Disposes resources uses by this DiffFinder + /// </summary> + //************************************************************************* + public override void Dispose() + { + base.Dispose(); + if (m_forwardHistory != null) + { + m_forwardHistory = null; + } + if (m_reverseHistory != null) + { + m_reverseHistory = null; + } + GC.SuppressFinalize(this); + } + + //************************************************************************* + /// <summary> + /// Computes the differences between the original and modified input + /// sequences on the bounded range. + /// </summary> + /// <returns>An array of the differences between the two input + /// sequences.</returns> + //************************************************************************* + protected override IDiffChange[] ComputeDiff(int originalStart, int originalEnd, + int modifiedStart, int modifiedEnd) + { + bool quitEarly; + IDiffChange[] changes = ComputeDiffRecursive(originalStart, originalEnd, modifiedStart, modifiedEnd, out quitEarly); + + // We have to clean up the computed diff to be more intuitive + // but it turns out this cannot be done correctly until the entire set + // of diffs have been computed + return ShiftChanges(changes); + } + + //************************************************************************* + /// <summary> + /// Private helper method which computes the differences on the bounded range + /// recursively. + /// </summary> + /// <returns>An array of the differences between the two input + /// sequences.</returns> + //************************************************************************* + private IDiffChange[] ComputeDiffRecursive(int originalStart, int originalEnd, + int modifiedStart, int modifiedEnd, + out bool quitEarly) + { + quitEarly = false; + + // Find the start of the differences + while (originalStart <= originalEnd && modifiedStart <= modifiedEnd && + ElementsAreEqual(originalStart, modifiedStart)) + { + originalStart++; + modifiedStart++; + } + + // Find the end of the differences + while (originalEnd >= originalStart && modifiedEnd >= modifiedStart && + ElementsAreEqual(originalEnd, modifiedEnd)) + { + originalEnd--; + modifiedEnd--; + } + + // In the special case where we either have all insertions or all deletions or the sequences are identical + if (originalStart > originalEnd || modifiedStart > modifiedEnd) + { + IDiffChange[] changes; + + if (modifiedStart <= modifiedEnd) + { + Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd"); + + // All insertions + changes = new IDiffChange[1]; + changes[0] = new DiffChange(originalStart, 0, + modifiedStart, + modifiedEnd - modifiedStart + 1); + } + else if (originalStart <= originalEnd) + { + Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); + + // All deletions + changes = new IDiffChange[1]; + changes[0] = new DiffChange(originalStart, + originalEnd - originalStart + 1, + modifiedStart, 0); + } + else + { + Debug.Assert(originalStart == originalEnd + 1, "originalStart should only be one more than originalEnd"); + Debug.Assert(modifiedStart == modifiedEnd + 1, "modifiedStart should only be one more than modifiedEnd"); + + // Identical sequences - No differences + changes = new IDiffChange[0]; + } + + return changes; + } + + // This problem can be solved using the Divide-And-Conquer technique. + int midOriginal, midModified; + IDiffChange[] result = ComputeRecursionPoint(originalStart, originalEnd, modifiedStart, modifiedEnd, + out midOriginal, out midModified, out quitEarly); + + if (result != null) + { + // Result is not-null when there was enough memory to compute the changes while + // searching for the recursion point + return result; + } + else if (!quitEarly) + { + // We can break the problem down recursively by finding the changes in the + // First Half: (originalStart, modifiedStart) to (midOriginal, midModified) + // Second Half: (midOriginal + 1, minModified + 1) to (originalEnd, modifiedEnd) + // NOTE: ComputeDiff() is inclusive, therefore the second range starts on the next point + IDiffChange[] leftChanges = ComputeDiffRecursive(originalStart, midOriginal, modifiedStart, midModified, out quitEarly); + IDiffChange[] rightChanges = new IDiffChange[0]; + + if (!quitEarly) + { + rightChanges = ComputeDiffRecursive(midOriginal + 1, originalEnd, midModified + 1, modifiedEnd, out quitEarly); + } + else + { + // We did't have time to finish the first half, so we don't have time to compute this half. + // Consider the entire rest of the sequence different. + rightChanges = new DiffChange[] + { + new DiffChange(midOriginal + 1, originalEnd - (midOriginal + 1) + 1, + midModified + 1, modifiedEnd - (midModified + 1) + 1) + }; + } + + return ConcatenateChanges(leftChanges, rightChanges); + } + + // If we hit here, we quit early, and so can't return anything meaningful + return new DiffChange[] + { + new DiffChange(originalStart, originalEnd -originalStart + 1, + modifiedStart, modifiedEnd - modifiedStart + 1) + }; + } + + //************************************************************************* + /// <summary> + /// Given the range to compute the diff on, this method finds the point: + /// (midOriginal, midModified) + /// that exists in the middle of the LCS of the two sequences and + /// is the point at which the LCS problem may be broken down recursively. + /// This method will try to keep the LCS trace in memory. If the LCS recursion + /// point is calculated and the full trace is available in memory, then this method + /// will return the change list. + /// </summary> + /// <param name="originalStart">The start bound of the original sequence range</param> + /// <param name="originalEnd">The end bound of the original sequence range</param> + /// <param name="modifiedStart">The start bound of the modified sequence range</param> + /// <param name="modifiedEnd">The end bound of the modified sequence range</param> + /// <param name="midOriginal">The middle point of the original sequence range</param> + /// <param name="midModified">The middle point of the modified sequence range</param> + /// <returns>The diff changes, if available, otherwise null</returns> + //************************************************************************* + private IDiffChange[] ComputeRecursionPoint(int originalStart, int originalEnd, + int modifiedStart, int modifiedEnd, + out int midOriginal, out int midModified, + out bool quitEarly) + { + int originalIndex, modifiedIndex; + int diagonalForwardStart = 0, diagonalForwardEnd = 0; + int diagonalReverseStart = 0, diagonalReverseEnd = 0; + int numDifferences; + + // To traverse the edit graph and produce the proper LCS, our actual + // start position is just outside the given boundary + originalStart--; + modifiedStart--; + + // We set these up to make the compiler happy, but they will + // be replaced before we return with the actual recursion point + midOriginal = 0; + midModified = 0; + + // Clear out the history + m_forwardHistory.Clear(); + m_reverseHistory.Clear(); + + // Each cell in the two arrays corresponds to a diagonal in the edit graph. + // The integer value in the cell represents the originalIndex of the furthest + // reaching point found so far that ends in that diagonal. + // The modifiedIndex can be computed mathematically from the originalIndex and the diagonal number. + int maxDifferences = (originalEnd - originalStart) + (modifiedEnd - modifiedStart); + int numDiagonals = maxDifferences + 1; + int[] forwardPoints = new int[numDiagonals]; + int[] reversePoints = new int[numDiagonals]; + // diagonalForwardBase: Index into forwardPoints of the diagonal which passes through (originalStart, modifiedStart) + // diagonalReverseBase: Index into reversePoints of the diagonal which passes through (originalEnd, modifiedEnd) + int diagonalForwardBase = (modifiedEnd - modifiedStart); + int diagonalReverseBase = (originalEnd - originalStart); + // diagonalForwardOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the + // diagonal number (relative to diagonalForwardBase) + // diagonalReverseOffset: Geometric offset which allows modifiedIndex to be computed from originalIndex and the + // diagonal number (relative to diagonalReverseBase) + int diagonalForwardOffset = (originalStart - modifiedStart); + int diagonalReverseOffset = (originalEnd - modifiedEnd); + + // delta: The difference between the end diagonal and the start diagonal. This is used to relate diagonal numbers + // relative to the start diagonal with diagonal numbers relative to the end diagonal. + // The Even/Oddn-ness of this delta is important for determining when we should check for overlap + int delta = diagonalReverseBase - diagonalForwardBase; + bool deltaIsEven = (delta % 2 == 0); + + // Here we set up the start and end points as the furthest points found so far + // in both the forward and reverse directions, respectively + forwardPoints[diagonalForwardBase] = originalStart; + reversePoints[diagonalReverseBase] = originalEnd; + + // Remember if we quit early, and thus need to do a best-effort result instead of a real result. + quitEarly = false; + + // A couple of points: + // --With this method, we iterate on the number of differences between the two sequences. + // The more differences there actually are, the longer this will take. + // --Also, as the number of differences increases, we have to search on diagonals further + // away from the reference diagonal (which is diagonalForwardBase for forward, diagonalReverseBase for reverse). + // --We extend on even diagonals (relative to the reference diagonal) only when numDifferences + // is even and odd diagonals only when numDifferences is odd. + for (numDifferences = 1; numDifferences <= (maxDifferences / 2) + 1; numDifferences++) + { + int furthestOriginalIndex = 0; + int furthestModifiedIndex = 0; + + // Run the algorithm in the forward direction + diagonalForwardStart = ClipDiagonalBound(diagonalForwardBase - numDifferences, + numDifferences, + diagonalForwardBase, + numDiagonals); + diagonalForwardEnd = ClipDiagonalBound(diagonalForwardBase + numDifferences, + numDifferences, + diagonalForwardBase, + numDiagonals); + for (int diagonal = diagonalForwardStart; diagonal <= diagonalForwardEnd; diagonal += 2) + { + // STEP 1: We extend the furthest reaching point in the present diagonal + // by looking at the diagonals above and below and picking the one whose point + // is further away from the start point (originalStart, modifiedStart) + if (diagonal == diagonalForwardStart || (diagonal < diagonalForwardEnd && + forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])) + { + originalIndex = forwardPoints[diagonal + 1]; + } + else + { + originalIndex = forwardPoints[diagonal - 1] + 1; + } + modifiedIndex = originalIndex - (diagonal - diagonalForwardBase) - diagonalForwardOffset; + + // Save the current originalIndex so we can test for false overlap in step 3 + int tempOriginalIndex = originalIndex; + + // STEP 2: We can continue to extend the furthest reaching point in the present diagonal + // so long as the elements are equal. + while (originalIndex < originalEnd && modifiedIndex < modifiedEnd + && ElementsAreEqual(originalIndex + 1, modifiedIndex + 1)) + { + originalIndex++; + modifiedIndex++; + } + forwardPoints[diagonal] = originalIndex; + + if (originalIndex + modifiedIndex > furthestOriginalIndex + furthestModifiedIndex) + { + furthestOriginalIndex = originalIndex; + furthestModifiedIndex = modifiedIndex; + } + + // STEP 3: If delta is odd (overlap first happens on forward when delta is odd) + // and diagonal is in the range of reverse diagonals computed for numDifferences-1 + // (the previous iteration; we havent computed reverse diagonals for numDifferences yet) + // then check for overlap. + if (!deltaIsEven && Math.Abs(diagonal - diagonalReverseBase) <= (numDifferences - 1)) + { + if (originalIndex >= reversePoints[diagonal]) + { + midOriginal = originalIndex; + midModified = modifiedIndex; + + if (tempOriginalIndex <= reversePoints[diagonal] + && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1)) + { + // BINGO! We overlapped, and we have the full trace in memory! + goto WALKTRACE; + } + else + { + // Either false overlap, or we didn't have enough memory for the full trace + // Just return the recursion point + return null; + } + } + } + } + + // Check to see if we should be quitting early, before moving on to the next iteration. + int matchLengthOfLongest = + ((furthestOriginalIndex - originalStart) + (furthestModifiedIndex - modifiedStart) - numDifferences) / 2; + + if (ContinueDifferencePredicate != null && + !ContinueDifferencePredicate(furthestOriginalIndex, OriginalSequence, matchLengthOfLongest)) + { + // We can't finish, so skip ahead to generating a result from what we have. + quitEarly = true; + + // Use the furthest distance we got in the forward direction. + midOriginal = furthestOriginalIndex; + midModified = furthestModifiedIndex; + + if (matchLengthOfLongest > 0 && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1)) + { + // Enough of the history is in memory to walk it backwards + goto WALKTRACE; + } + else + { + // We didn't actually remember enough of the history. + + //Since we are quiting the diff early, we need to shift back the originalStart and modified start + //back into the boundary limits since we decremented their value above beyond the boundary limit. + originalStart++; + modifiedStart++; + + return new DiffChange[] + { + new DiffChange(originalStart, originalEnd - originalStart + 1, + modifiedStart, modifiedEnd - modifiedStart + 1) + }; + } + } + + // Run the algorithm in the reverse direction + diagonalReverseStart = ClipDiagonalBound(diagonalReverseBase - numDifferences, + numDifferences, + diagonalReverseBase, + numDiagonals); + diagonalReverseEnd = ClipDiagonalBound(diagonalReverseBase + numDifferences, + numDifferences, + diagonalReverseBase, + numDiagonals); + for (int diagonal = diagonalReverseStart; diagonal <= diagonalReverseEnd; diagonal += 2) + { + // STEP 1: We extend the furthest reaching point in the present diagonal + // by looking at the diagonals above and below and picking the one whose point + // is further away from the start point (originalEnd, modifiedEnd) + if (diagonal == diagonalReverseStart || (diagonal < diagonalReverseEnd && + reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])) + { + originalIndex = reversePoints[diagonal + 1] - 1; + } + else + { + originalIndex = reversePoints[diagonal - 1]; + } + modifiedIndex = originalIndex - (diagonal - diagonalReverseBase) - diagonalReverseOffset; + + // Save the current originalIndex so we can test for false overlap + int tempOriginalIndex = originalIndex; + + // STEP 2: We can continue to extend the furthest reaching point in the present diagonal + // as long as the elements are equal. + while (originalIndex > originalStart && modifiedIndex > modifiedStart + && ElementsAreEqual(originalIndex, modifiedIndex)) + { + originalIndex--; + modifiedIndex--; + } + reversePoints[diagonal] = originalIndex; + + // STEP 4: If delta is even (overlap first happens on reverse when delta is even) + // and diagonal is in the range of forward diagonals computed for numDifferences + // then check for overlap. + if (deltaIsEven && Math.Abs(diagonal - diagonalForwardBase) <= numDifferences) + { + if (originalIndex <= forwardPoints[diagonal]) + { + midOriginal = originalIndex; + midModified = modifiedIndex; + + if (tempOriginalIndex >= forwardPoints[diagonal] + && MaxDifferencesHistory > 0 && numDifferences <= (MaxDifferencesHistory + 1)) + { + // BINGO! We overlapped, and we have the full trace in memory! + goto WALKTRACE; + } + else + { + // Either false overlap, or we didn't have enough memory for the full trace + // Just return the recursion point + return null; + } + } + } + } + + // Save current vectors to history before the next iteration + if (numDifferences <= MaxDifferencesHistory) + { + // We are allocating space for one extra int, which we fill with + // the index of the diagonal base index + int[] temp = new int[diagonalForwardEnd - diagonalForwardStart + 2]; + temp[0] = diagonalForwardBase - diagonalForwardStart + 1; + Array.Copy(forwardPoints, diagonalForwardStart, temp, 1, diagonalForwardEnd - diagonalForwardStart + 1); + m_forwardHistory.Add(temp); + + temp = new int[diagonalReverseEnd - diagonalReverseStart + 2]; + temp[0] = diagonalReverseBase - diagonalReverseStart + 1; + Array.Copy(reversePoints, diagonalReverseStart, temp, 1, diagonalReverseEnd - diagonalReverseStart + 1); + m_reverseHistory.Add(temp); + } + } + + // If we got here, then we have the full trace in history. We just have to convert it to a change list + // NOTE: This part is a bit messy + WALKTRACE: IDiffChange[] forwardChanges, reverseChanges; + + // First, walk backward through the forward diagonals history + using (DiffChangeHelper changeHelper = new DiffChangeHelper()) + { + int diagonalMin = diagonalForwardStart; + int diagonalMax = diagonalForwardEnd; + int diagonalRelative = (midOriginal - midModified) - diagonalForwardOffset; + int lastOriginalIndex = Int32.MinValue; + int historyIndex = m_forwardHistory.Count - 1; + + do + { + // Get the diagonal index from the relative diagonal number + int diagonal = diagonalRelative + diagonalForwardBase; + + // Figure out where we came from + if (diagonal == diagonalMin || (diagonal < diagonalMax && + forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])) + { + // Vertical line (the element is an insert) + originalIndex = forwardPoints[diagonal + 1]; + modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset; + if (originalIndex < lastOriginalIndex) + { + changeHelper.MarkNextChange(); + } + lastOriginalIndex = originalIndex; + changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex); + diagonalRelative = (diagonal + 1) - diagonalForwardBase; //Setup for the next iteration + } + else + { + // Horizontal line (the element is a deletion) + originalIndex = forwardPoints[diagonal - 1] + 1; + modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset; + if (originalIndex < lastOriginalIndex) + { + changeHelper.MarkNextChange(); + } + lastOriginalIndex = originalIndex - 1; + changeHelper.AddOriginalElement(originalIndex, modifiedIndex + 1); + diagonalRelative = (diagonal - 1) - diagonalForwardBase; //Setup for the next iteration + } + + if (historyIndex >= 0) + { + forwardPoints = m_forwardHistory[historyIndex]; + diagonalForwardBase = forwardPoints[0]; //We stored this in the first spot + diagonalMin = 1; + diagonalMax = forwardPoints.Length - 1; + } + } while (--historyIndex >= -1); + + // Ironically, we get the forward changes as the reverse of the + // order we added them since we technically added them backwards + forwardChanges = changeHelper.ReverseChanges; + } + + if (quitEarly) + { + // Since we did quit early, assume everything after the midOriginal/midModified point is a diff + + int originalStartPoint = midOriginal + 1; + int modifiedStartPoint = midModified + 1; + + if (forwardChanges != null && forwardChanges.Length > 0) + { + IDiffChange lastForwardChange = forwardChanges[forwardChanges.Length - 1]; + originalStartPoint = Math.Max(originalStartPoint, lastForwardChange.OriginalEnd); + modifiedStartPoint = Math.Max(modifiedStartPoint, lastForwardChange.ModifiedEnd); + } + + reverseChanges = new DiffChange[] + { + new DiffChange(originalStartPoint, originalEnd - originalStartPoint + 1, + modifiedStartPoint, modifiedEnd - modifiedStartPoint + 1) + }; + } + else + { + // Now walk backward through the reverse diagonals history + using (DiffChangeHelper changeHelper = new DiffChangeHelper()) + { + int diagonalMin = diagonalReverseStart; + int diagonalMax = diagonalReverseEnd; + int diagonalRelative = (midOriginal - midModified) - diagonalReverseOffset; + int lastOriginalIndex = Int32.MaxValue; + int historyIndex = (deltaIsEven) ? m_reverseHistory.Count - 1 + : m_reverseHistory.Count - 2; + + do + { + // Get the diagonal index from the relative diagonal number + int diagonal = diagonalRelative + diagonalReverseBase; + + // Figure out where we came from + if (diagonal == diagonalMin || (diagonal < diagonalMax && + reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])) + { + // Horizontal line (the element is a deletion)) + originalIndex = reversePoints[diagonal + 1] - 1; + modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset; + if (originalIndex > lastOriginalIndex) + { + changeHelper.MarkNextChange(); + } + lastOriginalIndex = originalIndex + 1; + changeHelper.AddOriginalElement(originalIndex + 1, modifiedIndex + 1); + diagonalRelative = (diagonal + 1) - diagonalReverseBase; //Setup for the next iteration + } + else + { + // Vertical line (the element is an insertion) + originalIndex = reversePoints[diagonal - 1]; + modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset; + if (originalIndex > lastOriginalIndex) + { + changeHelper.MarkNextChange(); + } + lastOriginalIndex = originalIndex; + changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex + 1); + diagonalRelative = (diagonal - 1) - diagonalReverseBase; //Setup for the next iteration + } + + if (historyIndex >= 0) + { + reversePoints = m_reverseHistory[historyIndex]; + diagonalReverseBase = reversePoints[0]; //We stored this in the first spot + diagonalMin = 1; + diagonalMax = reversePoints.Length - 1; + } + } while (--historyIndex >= -1); + + // There are cases where the reverse history will find diffs that + // are correct, but not intuitive, so we need shift them. + reverseChanges = changeHelper.Changes; + } + } + + return ConcatenateChanges(forwardChanges, reverseChanges); + } + + //********************************************************************* + /// <summary> + /// Shifts the given changes to provide a more intuitive diff. + /// While the first element in a diff matches the first element after the diff, + /// we shift the diff down. + /// </summary> + /// <param name="changes">The list of changes to shift</param> + /// <returns>The shifted changes</returns> + //********************************************************************* + private IDiffChange[] ShiftChanges(IDiffChange[] changes) + { + // Shift all the changes first + for (int i = 0; i < changes.Length; i++) + { + Debug.Assert(changes[i] is DiffChange, "change is not a DiffChange"); + + DiffChange change = changes[i] as DiffChange; + int originalStop = (i < changes.Length - 1) ? changes[i + 1].OriginalStart : OriginalSequence.Count; + int modifiedStop = (i < changes.Length - 1) ? changes[i + 1].ModifiedStart : ModifiedSequence.Count; + bool checkOriginal = change.OriginalLength > 0; + bool checkModified = change.ModifiedLength > 0; + + while (change.OriginalStart + change.OriginalLength < originalStop && + change.ModifiedStart + change.ModifiedLength < modifiedStop && + (!checkOriginal || OriginalElementsAreEqual(change.OriginalStart, change.OriginalStart + change.OriginalLength)) && + (!checkModified || ModifiedElementsAreEqual(change.ModifiedStart, change.ModifiedStart + change.ModifiedLength))) + { + change.OriginalStart++; + change.ModifiedStart++; + } + } + + // Build up the new list (we have to build a new list because we + // might have changes we can merge together now) + List<IDiffChange> result = new List<IDiffChange>(changes.Length); + IDiffChange mergedChange; + for (int i = 0; i < changes.Length; i++) + { + if (i < changes.Length - 1 && ChangesOverlap(changes[i], changes[i + 1], out mergedChange)) + { + result.Add(mergedChange); + i++; + } + else + { + result.Add(changes[i]); + } + } + + return result.ToArray(); + } + + //************************************************************************* + /// <summary> + /// Concatentates the two input DiffChange lists and returns the resulting + /// list. + /// </summary> + /// <param name="left">The left changes</param> + /// <param name="right">The right changes</param> + /// <returns>The concatenated list</returns> + //************************************************************************* + private IDiffChange[] ConcatenateChanges(IDiffChange[] left, IDiffChange[] right) + { + IDiffChange mergedChange; + + if (left.Length == 0 || right.Length == 0) + { + return (right.Length > 0) ? right : left; + } + else if (ChangesOverlap(left[left.Length - 1], right[0], out mergedChange)) + { + // Since we break the problem down recursively, it is possible that we + // might recurse in the middle of a change thereby splitting it into + // two changes. Here in the combining stage, we detect and fuse those + // changes back together + IDiffChange[] result = new IDiffChange[left.Length + right.Length - 1]; + Array.Copy(left, 0, result, 0, left.Length - 1); + result[left.Length - 1] = mergedChange; + Array.Copy(right, 1, result, left.Length, right.Length - 1); + + return result; + } + else + { + IDiffChange[] result = new IDiffChange[left.Length + right.Length]; + Array.Copy(left, 0, result, 0, left.Length); + Array.Copy(right, 0, result, left.Length, right.Length); + + return result; + } + } + + //************************************************************************* + /// <summary> + /// Returns true if the two changes overlap and can be merged into a single + /// change + /// </summary> + /// <param name="left">The left change</param> + /// <param name="right">The right change</param> + /// <param name="mergedChange">The merged change if the two overlap, + /// null otherwise</param> + /// <returns>True if the two changes overlap</returns> + //************************************************************************* + private bool ChangesOverlap(IDiffChange left, IDiffChange right, out IDiffChange mergedChange) + { + Debug.Assert(left.OriginalStart <= right.OriginalStart, "Left change is not less than or equal to right change"); + Debug.Assert(left.ModifiedStart <= right.ModifiedStart, "Left change is not less than or equal to right change"); + + if (left.OriginalStart + left.OriginalLength >= right.OriginalStart + || left.ModifiedStart + left.ModifiedLength >= right.ModifiedStart) + { + int originalStart = left.OriginalStart; + int originalLength = left.OriginalLength; + int modifiedStart = left.ModifiedStart; + int modifiedLength = left.ModifiedLength; + + if (left.OriginalStart + left.OriginalLength >= right.OriginalStart) + { + originalLength = right.OriginalStart + right.OriginalLength - left.OriginalStart; + } + if (left.ModifiedStart + left.ModifiedLength >= right.ModifiedStart) + { + modifiedLength = right.ModifiedStart + right.ModifiedLength - left.ModifiedStart; + } + + mergedChange = new DiffChange(originalStart, originalLength, modifiedStart, modifiedLength); + return true; + } + else + { + mergedChange = null; + return false; + } + } + + //************************************************************************* + /// <summary> + /// Helper method used to clip a diagonal index to the range of valid + /// diagonals. This also decides whether or not the diagonal index, + /// if it exceeds the boundary, should be clipped to the boundary or clipped + /// one inside the boundary depending on the Even/Odd status of the boundary + /// and numDifferences. + /// </summary> + /// <param name="diagonal">The index of the diagonal to clip.</param> + /// <param name="numDifferences">The current number of differences being + /// iterated upon.</param> + /// <param name="diagonalBaseIndex">The base reference diagonal.</param> + /// <param name="numDiagonals">The total number of diagonals.</param> + /// <returns>The clipped diagonal index.</returns> + //************************************************************************* + private int ClipDiagonalBound(int diagonal, + int numDifferences, + int diagonalBaseIndex, + int numDiagonals) + { + if (diagonal >= 0 && diagonal < numDiagonals) + { + // Nothing to clip, its in range + return diagonal; + } + + // diagonalsBelow: The number of diagonals below the reference diagonal + // diagonalsAbove: The number of diagonals above the reference diagonal + int diagonalsBelow = diagonalBaseIndex; + int diagonalsAbove = numDiagonals - diagonalBaseIndex - 1; + bool diffEven = (numDifferences % 2 == 0); + + if (diagonal < 0) + { + bool lowerBoundEven = (diagonalsBelow % 2 == 0); + return (diffEven == lowerBoundEven) ? 0 : 1; + } + else + { + bool upperBoundEven = (diagonalsAbove % 2 == 0); + return (diffEven == upperBoundEven) ? numDiagonals - 1 : numDiagonals - 2; + } + } + + // Member variables + private List<int[]> m_forwardHistory; + private List<int[]> m_reverseHistory; + + // Our total memory usage for storing history is (worst-case): + // 2 * [(MaxDifferencesHistory + 1) * (MaxDifferencesHistory + 1) - 1] * sizeof(int) + // 2 * [1448*1448 - 1] * 4 = 16773624 = 16MB + private const int MaxDifferencesHistory = 1447; + } +} diff --git a/src/Text/Impl/DifferenceAlgorithm/TextDifferencingSelectorService.cs b/src/Text/Impl/DifferenceAlgorithm/TextDifferencingSelectorService.cs new file mode 100644 index 0000000..5a27014 --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/TextDifferencingSelectorService.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Linq; +using System.Text; +using Microsoft.VisualStudio.Text.Utilities; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + [Export(typeof(ITextDifferencingSelectorService))] + class TextDifferencingSelectorService : ITextDifferencingSelectorService + { + [ImportMany(typeof(ITextDifferencingService))] + internal List<Lazy<ITextDifferencingService, IContentTypeMetadata>> _textDifferencingServices { get; set; } + + [Import] + internal IContentTypeRegistryService _contentTypeRegistryService { get; set; } + + [Import] + internal GuardedOperations _guardedOperations { get; set; } + + public ITextDifferencingService GetTextDifferencingService(IContentType contentType) + { + ITextDifferencingService service = + _guardedOperations.InvokeBestMatchingFactory + (_textDifferencingServices, + contentType, + differencingService => differencingService, + _contentTypeRegistryService, this); + + return service ?? DefaultTextDifferencingService; + } + + static DefaultTextDifferencingService _defaultTextDifferencingService = new DefaultTextDifferencingService(); + public ITextDifferencingService DefaultTextDifferencingService { get { return _defaultTextDifferencingService; } } + } +} diff --git a/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs b/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs new file mode 100644 index 0000000..f7afc1c --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/TokenizedStringList.cs @@ -0,0 +1,270 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + internal interface ITokenizedStringListInternal : ITokenizedStringList + { + string OriginalSubstring(int startIndex, int length); + } + + /// <summary> + /// Tokenizes the string into abutting and non-overlapping segments. + /// </summary> + /// <remarks> + /// This class implements IList so that it can be used with + /// <see cref="IDifferenceService" />, which finds the differences between two sequences represented + /// as ILists. + /// Most of the members of the IList interface are unimplemented. The only + /// implemented methods are the array accessor getter (operator []), Count, + /// and IsReadOnly. + /// </remarks> + internal abstract class TokenizedStringList : ITokenizedStringListInternal + { + protected List<Span> Tokens = new List<Span>(); + private readonly string original; + private readonly SnapshotSpan originalSpan; + + /// <summary> + /// Creates a tokenized string list from the original string. + /// Any derived class must initialize the Tokens list in its own constructor. + /// </summary> + /// <param name="original">The original string.</param> + protected TokenizedStringList(string original) + { + if (original == null) + throw new ArgumentNullException("original"); + + this.original = original; + } + + protected TokenizedStringList(SnapshotSpan originalSpan) + { + this.originalSpan = originalSpan; + } + + /// <summary> + /// The original string that was tokenized. + /// </summary> + public string Original + { + get + { + // A call to GetText() here could be very expensive in memory. Be careful! + return original ?? originalSpan.GetText(); + } + } + + internal int OriginalLength + { + get + { + return (original != null) ? original.Length : originalSpan.Length; + } + } + + public string OriginalSubstring(int startIndex, int length) + { + if (original != null) + { + return original.Substring(startIndex, length); + } + else + { + ITextSnapshot snap = originalSpan.Snapshot; + return snap.GetText(originalSpan.Start.Position + startIndex, length); + } + } + + /// <summary> + /// Maps the index of an element to its span in the original list. + /// </summary> + /// <param name="index">The index of the element in the element list.</param> + /// <returns>The span of the element.</returns> + /// <exception cref="ArgumentOutOfRangeException">The specified index is either negative or exceeds the list's Count property.</exception> + /// <remarks>This method returns a zero-length span at the end of the string if index + /// is equal to the list's Count property.</remarks> + public Span GetElementInOriginal(int index) + { + //Pure support for backward compatibility + if (index == this.Count) + { + return new Span(this.OriginalLength, 0); + } + + return this.Tokens[index]; + } + + /// <summary> + /// Maps a span of elements in this list to the span in the original list. + /// </summary> + /// <param name="span">The span of elements in the elements list.</param> + /// <returns>The span mapped onto the original list.</returns> + public Span GetSpanInOriginal(Span span) + { + //Pure support for backward compatibility + if (span.Start == this.Tokens.Count) + { + return new Span(this.OriginalLength, 0); + } + + int start = this.Tokens[span.Start].Start; + int end = (span.Length == 0) ? start : this.Tokens[span.End - 1].End; + + return Span.FromBounds(start, end); + } + + /// <summary> + /// Gets a string of the given element. + /// </summary> + /// <param name="index">The index into the list of elements.</param> + /// <returns>The element, as a string.</returns> + /// <remarks>The setter of this property throws a NotImplementedException.</remarks> + public string this[int index] + { + get + { + // The out of range check will happen in GetElementInOriginal + Span span = GetElementInOriginal(index); + return this.OriginalSubstring(span.Start, span.Length); + } + set + { + throw new NotSupportedException(); + } + } + + internal char CharacterAt(int offset) + { + return (original != null) ? original[offset] : originalSpan.Snapshot[originalSpan.Start.Position + offset]; + } + + /// <summary> + /// The number of elements in the list. + /// </summary> + public int Count + { + get + { + return this.Tokens.Count; + } + } + + /// <summary> + /// Determines whether this list is read-only. It always returnes <c>true</c>. + /// </summary> + public bool IsReadOnly + { + get { return true; } + } + +#region Not Implemented + /// <summary> + /// Not implemented + /// </summary> + /// <param name="item"></param> + /// <returns></returns> + public int IndexOf(string item) + { + throw new NotSupportedException(); + } + + /// <summary> + /// Not implemented. + /// </summary> + /// <param name="index"></param> + /// <param name="item"></param> + public void Insert(int index, string item) + { + throw new NotSupportedException(); + } + + /// <summary> + /// Not implemented. + /// </summary> + /// <param name="index"></param> + public void RemoveAt(int index) + { + throw new NotSupportedException(); + } + + /// <summary> + /// Not implemented. + /// </summary> + /// <param name="item"></param> + public void Add(string item) + { + throw new NotSupportedException(); + } + + /// <summary> + /// Not implemented. + /// </summary> + public void Clear() + { + throw new NotSupportedException(); + } + + /// <summary> + /// Not implemented. + /// </summary> + public bool Contains(string item) + { + throw new NotSupportedException(); + } + + /// <summary> + /// Not implemented. + /// </summary> + public void CopyTo(string[] array, int arrayIndex) + { + throw new NotSupportedException(); + } + + /// <summary> + /// Not implemented. + /// </summary> + public bool Remove(string item) + { + throw new NotSupportedException(); + } + +#endregion + +#region IEnumerable<string> Members + + /// <summary> + /// Gets the enumerator of type string. + /// </summary> + /// <returns>The enumerator of type string.</returns> + public IEnumerator<string> GetEnumerator() + { + for (int i = 0; i < Count; i++) + yield return this[i]; + } + +#endregion + +#region IEnumerable Members + + /// <summary> + /// Gets the untyped enumerator. + /// </summary> + /// <returns>The untyped enumerator.</returns> + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + +#endregion + } +} diff --git a/src/Text/Impl/DifferenceAlgorithm/WordDecompositionList.cs b/src/Text/Impl/DifferenceAlgorithm/WordDecompositionList.cs new file mode 100644 index 0000000..e676e73 --- /dev/null +++ b/src/Text/Impl/DifferenceAlgorithm/WordDecompositionList.cs @@ -0,0 +1,132 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Text; +using System.Globalization; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + // TODO: Replace the logic in this class with the upcoming Line/Word + // split utility. + + /// <summary> + /// This is a word decomposition of the given string. + /// </summary> + internal sealed class WordDecompositionList : TokenizedStringList + { + public WordDecompositionList(string original, StringDifferenceOptions options) + : base(original) + { + this.CreateTokens(options, ignoreTrimWhiteSpace: false); // We never paid attention to trim whitespace for strings when doing line or word diffs. + } + + public WordDecompositionList(SnapshotSpan original, StringDifferenceOptions options) + : base(original) + { + this.CreateTokens(options, options.IgnoreTrimWhiteSpace); + } + + private void CreateTokens(StringDifferenceOptions options, bool ignoreTrimWhiteSpace) + { + int end = this.OriginalLength; + int i = 0; + int tokenStart = 0; + TokenType previousTokenType = TokenType.WhiteSpace; + bool skipPreviousToken = ignoreTrimWhiteSpace; // Assume that the 1st whitespace token is ignoreable if we're trimming whitespace. + while (i < end) + { + bool skipNextToken; + TokenType nextTokenType; + int breakLength = this.LengthOfLineBreak(i, end); + if (breakLength != 0) + { + nextTokenType = ignoreTrimWhiteSpace ? TokenType.WhiteSpace : TokenType.LineBreak; + skipNextToken = ignoreTrimWhiteSpace; + } + else + { + nextTokenType = GetTokenType(this.CharacterAt(i), options.WordSplitBehavior); + skipNextToken = (nextTokenType == TokenType.WhiteSpace) ? skipPreviousToken : false; + breakLength = 1; + } + + if ((nextTokenType != previousTokenType) || (nextTokenType == TokenType.Symbol)) + { + if ((tokenStart < i) && !skipPreviousToken) + { + this.Tokens.Add(new Span(tokenStart, i - tokenStart)); + } + + previousTokenType = nextTokenType; + tokenStart = i; + } + + skipPreviousToken = skipNextToken; + i += breakLength; + } + + if ((end == 0) || // 0-length sequences get a single token + !(ignoreTrimWhiteSpace && (previousTokenType == TokenType.WhiteSpace))) // act as if there is an implicit line break at the end of the line. + { + this.Tokens.Add(new Span(tokenStart, end - tokenStart)); + } + } + + private enum TokenType + { + LineBreak, + WhiteSpace, + Symbol, + Digit, + Letter, + Other, + }; + + private static TokenType GetTokenType(char c, WordSplitBehavior splitBehavior) + { + if (char.IsWhiteSpace(c)) + return TokenType.WhiteSpace; + + if (splitBehavior == WordSplitBehavior.WhiteSpace) + return TokenType.Other; + + if (char.IsPunctuation(c) || char.IsSymbol(c)) + return TokenType.Symbol; + + if (splitBehavior == WordSplitBehavior.WhiteSpaceAndPunctuation) + return TokenType.Other; + + if (char.IsDigit(c) || char.IsNumber(c)) + return TokenType.Digit; + + if (char.IsLetter(c)) + return TokenType.Letter; + + return TokenType.Other; + } + + // This is roughly a copy of TextUtilities.LengthOfLineBreak (but using CharacterAt). + public int LengthOfLineBreak(int start, int end) + { + char c1 = this.CharacterAt(start); + if (c1 == '\r') + { + return ((++start < end) && (this.CharacterAt(start) == '\n')) ? 2 : 1; + } + else if ((c1 == '\n') || (c1 == '\u0085') || + (c1 == '\u2028' /*unicode line separator*/) || + (c1 == '\u2029' /*unicode paragraph separator*/)) + { + return 1; + } + else + return 0; + } + } +} diff --git a/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs b/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs new file mode 100644 index 0000000..97d1369 --- /dev/null +++ b/src/Text/Impl/EditorOperations/AfterTextBufferChangeUndoPrimitive.cs @@ -0,0 +1,261 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.Diagnostics; + using Microsoft.VisualStudio.Text.Editor; + + /// <summary> + /// The UndoPrimitive to take place on the Undo stack after a text buffer change. This is the simpler + /// version of the primitive that handles most common cases. + /// </summary> + internal class AfterTextBufferChangeUndoPrimitive : TextUndoPrimitive + { + // Think twice before adding any fields here! These objects are long-lived and consume considerable space. + // Unusual cases should be handled by the GeneralAfterTextBufferChangedUndoPrimitive class below. + protected ITextUndoHistory _undoHistory; + protected int _newCaretIndex; + protected byte _newCaretAffinityByte; + protected bool _canUndo; + + /// <summary> + /// Constructs a AfterTextBufferChangeUndoPrimitive. + /// </summary> + /// <param name="textView"> + /// The text view that was responsible for causing this change. + /// </param> + /// <param name="undoHistory"> + /// The <see cref="ITextUndoHistory" /> this primitive will be added to. + /// </param> + /// <exception cref="ArgumentNullException"><paramref name="textView"/> is null.</exception> + /// <exception cref="ArgumentNullException"><paramref name="undoHistory"/> is null.</exception> + public static AfterTextBufferChangeUndoPrimitive Create(ITextView textView, ITextUndoHistory undoHistory) + { + if (textView == null) + { + throw new ArgumentNullException("textView"); + } + if (undoHistory == null) + { + throw new ArgumentNullException("undoHistory"); + } + + // Store the ITextView for these changes in the ITextUndoHistory properties so we can retrieve it later. + if (!undoHistory.Properties.ContainsProperty(typeof(ITextView))) + { + undoHistory.Properties[typeof(ITextView)] = textView; + } + + IMapEditToData map = BeforeTextBufferChangeUndoPrimitive.GetMap(textView); + + CaretPosition caret = textView.Caret.Position; + int newCaretIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, caret.BufferPosition); + int newCaretVirtualSpaces = caret.VirtualBufferPosition.VirtualSpaces; + + VirtualSnapshotPoint anchor = textView.Selection.AnchorPoint; + int newSelectionAnchorIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, anchor.Position); + int newSelectionAnchorVirtualSpaces = anchor.VirtualSpaces; + + VirtualSnapshotPoint active = textView.Selection.ActivePoint; + int newSelectionActiveIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, active.Position); + int newSelectionActiveVirtualSpaces = active.VirtualSpaces; + + TextSelectionMode newSelectionMode = textView.Selection.Mode; + + if (newCaretVirtualSpaces != 0 || + newSelectionAnchorIndex != newCaretIndex || + newSelectionAnchorVirtualSpaces != 0 || + newSelectionActiveIndex != newCaretIndex || + newSelectionActiveVirtualSpaces != 0 || + newSelectionMode != TextSelectionMode.Stream) + { + return new GeneralAfterTextBufferChangeUndoPrimitive + (undoHistory, newCaretIndex, caret.Affinity, newCaretVirtualSpaces, newSelectionAnchorIndex, + newSelectionAnchorVirtualSpaces, newSelectionActiveIndex, newSelectionActiveVirtualSpaces, newSelectionMode); + } + else + { + return new AfterTextBufferChangeUndoPrimitive(undoHistory, newCaretIndex, caret.Affinity); + } + } + + protected AfterTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, int caretIndex, PositionAffinity caretAffinity) + { + _undoHistory = undoHistory; + _newCaretIndex = caretIndex; + _newCaretAffinityByte = (byte)caretAffinity; + _canUndo = true; + } + + // Internal empty constructor for unit testing. + internal AfterTextBufferChangeUndoPrimitive() { } + + /// <summary> + /// The <see cref="ITextView"/> that this <see cref="AfterTextBufferChangeUndoPrimitive"/> is bound to. + /// </summary> + internal ITextView GetTextView() + { + ITextView view = null; + _undoHistory.Properties.TryGetProperty(typeof(ITextView), out view); + return view; + } + + internal int CaretIndex + { + get { return _newCaretIndex; } + } + + internal virtual int CaretVirtualSpace + { + get { return 0; } + } + + #region ITextUndoPrimitive Members + + /// <summary> + /// Returns true if operation can be undone, false otherwise. + /// </summary> + public override bool CanUndo + { + get { return _canUndo; } + } + + /// <summary> + /// Returns true if operation can be redone, false otherwise. + /// </summary> + public override bool CanRedo + { + get { return !_canUndo; } + } + + /// <summary> + /// Do the action. + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be redone.</exception> + public override void Do() + { + // Validate, we shouldn't be allowed to undo + if (!CanRedo) + { + throw new InvalidOperationException(Strings.CannotRedo); + } + + // Set the new caret position and active selection + var view = this.GetTextView(); + Debug.Assert(view == null || !view.IsClosed, "Attempt to undo/redo on a closed view? This shouldn't happen."); + if (view != null && !view.IsClosed) + { + DoMoveCaretAndSelect(view, BeforeTextBufferChangeUndoPrimitive.GetMap(view)); + view.Caret.EnsureVisible(); + } + + _canUndo = true; + } + + /// <summary> + /// Move the caret and restore the selection as part of the Redo operation. + /// </summary> + protected virtual void DoMoveCaretAndSelect(ITextView view, IMapEditToData map) + { + SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newCaretIndex)); + + view.Caret.MoveTo(newCaret, (PositionAffinity)_newCaretAffinityByte); + view.Selection.Clear(); + } + + /// <summary> + /// Undo the action. + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception> + public override void Undo() + { + // Validate that we can undo this change + if (!CanUndo) + { + throw new InvalidOperationException(Strings.CannotUndo); + } + + // Currently, no action is done on the Undo. To restore the caret and selection after a text buffer undo, there is the BeforeTextBufferChangeUndoPrimitive. + // This undo should not do anything with the caret and selection, because we only want to reset them after the TextBuffer change has occurred. + // Therefore, we need to add this UndoPrimitive to the undo stack before the UndoPrimitive for the TextBuffer change. On an redo, the TextBuffer changed UndoPrimitive + // will fire it's Redo first, and than the Redo for this UndoPrimitive will fire. + // However, on an undo, the undo for this UndoPrimitive will be fired, and then the undo for the TextBuffer change UndoPrimitive. If we had set any caret placement/selection here (ie the old caret placement/selection), + // we may crash because the TextBuffer change has not occurred yet (ie you try to set the caret to be at CharacterIndex 1 when the TextBuffer is still empty). + + _canUndo = false; + } + + public override bool CanMerge(ITextUndoPrimitive older) + { + return false; + } + #endregion + } + + /// <summary> + /// The UndoPrimitive to take place on the Undo stack before a text buffer change. This is the general + /// version of the primitive that handles all cases, including those involving selections and virtual space. + /// </summary> + internal class GeneralAfterTextBufferChangeUndoPrimitive : AfterTextBufferChangeUndoPrimitive + { + private int _newCaretVirtualSpaces; + private int _newSelectionAnchorIndex; + private int _newSelectionAnchorVirtualSpaces; + private int _newSelectionActiveIndex; + private int _newSelectionActiveVirtualSpaces; + private TextSelectionMode _newSelectionMode; + + public GeneralAfterTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, + int newCaretIndex, + PositionAffinity newCaretAffinity, + int newCaretVirtualSpaces, + int newSelectionAnchorIndex, + int newSelectionAnchorVirtualSpaces, + int newSelectionActiveIndex, + int newSelectionActiveVirtualSpaces, + TextSelectionMode newSelectionMode) + : base(undoHistory, newCaretIndex, newCaretAffinity) + { + _newCaretVirtualSpaces = newCaretVirtualSpaces; + _newSelectionAnchorIndex = newSelectionAnchorIndex; + _newSelectionAnchorVirtualSpaces = newSelectionAnchorVirtualSpaces; + _newSelectionActiveIndex = newSelectionActiveIndex; + _newSelectionActiveVirtualSpaces = newSelectionActiveVirtualSpaces; + _newSelectionMode = newSelectionMode; + } + + internal override int CaretVirtualSpace + { + get { return _newCaretVirtualSpaces; } + } + + /// <summary> + /// Move the caret and restore the selection as part of the Redo operation. + /// </summary> + protected override void DoMoveCaretAndSelect(ITextView view, IMapEditToData map) + { + SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newCaretIndex)); + SnapshotPoint newAnchor = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newSelectionAnchorIndex)); + SnapshotPoint newActive = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _newSelectionActiveIndex)); + + view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret, _newCaretVirtualSpaces), (PositionAffinity)_newCaretAffinityByte); + + view.Selection.Mode = _newSelectionMode; + + var virtualAnchor = new VirtualSnapshotPoint(newAnchor, _newSelectionAnchorVirtualSpaces); + var virtualActive = new VirtualSnapshotPoint(newActive, _newSelectionActiveVirtualSpaces); + + // Buffer may have been changed by one of the listeners on the caret move event. + virtualAnchor = virtualAnchor.TranslateTo(view.TextSnapshot); + virtualActive = virtualActive.TranslateTo(view.TextSnapshot); + + view.Selection.Select(virtualAnchor, virtualActive); + } + } +} diff --git a/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs b/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs new file mode 100644 index 0000000..47abd9b --- /dev/null +++ b/src/Text/Impl/EditorOperations/BeforeTextBufferChangeUndoPrimitive.cs @@ -0,0 +1,298 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.Diagnostics; + using Microsoft.VisualStudio.Text.Editor; + + /// <summary> + /// The UndoPrimitive to take place on the Undo stack before a text buffer change. This is the simpler + /// version of the primitive that handles most common cases. + /// </summary> + internal class BeforeTextBufferChangeUndoPrimitive : TextUndoPrimitive + { + // Think twice before adding any fields here! These objects are long-lived and consume considerable space. + // Unusual cases should be handled by the GeneralAfterTextBufferChangedUndoPrimitive class below. + protected ITextUndoHistory _undoHistory; + protected int _oldCaretIndex; + protected byte _oldCaretAffinityByte; + protected bool _canUndo; + + /// <summary> + /// Constructs a BeforeTextBufferChangeUndoPrimitive. + /// </summary> + /// <param name="textView"> + /// The text view that was responsible for causing this change. + /// </param> + /// <param name="undoHistory"> + /// The <see cref="ITextUndoHistory" /> this primitive will be added to. + /// </param> + /// <exception cref="ArgumentNullException"><paramref name="textView"/> is null.</exception> + /// <exception cref="ArgumentNullException"><paramref name="undoHistory"/> is null.</exception> + public static BeforeTextBufferChangeUndoPrimitive Create(ITextView textView, ITextUndoHistory undoHistory) + { + if (textView == null) + { + throw new ArgumentNullException("textView"); + } + if (undoHistory == null) + { + throw new ArgumentNullException("undoHistory"); + } + + // Store the ITextView for these changes in the ITextUndoHistory properties so we can retrieve it later. + if (!undoHistory.Properties.ContainsProperty(typeof(ITextView))) + { + undoHistory.Properties[typeof(ITextView)] = textView; + } + + CaretPosition caret = textView.Caret.Position; + + IMapEditToData map = BeforeTextBufferChangeUndoPrimitive.GetMap(textView); + + int oldCaretIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, caret.BufferPosition); + int oldCaretVirtualSpaces = caret.VirtualBufferPosition.VirtualSpaces; + + VirtualSnapshotPoint anchor = textView.Selection.AnchorPoint; + int oldSelectionAnchorIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, anchor.Position); + int oldSelectionAnchorVirtualSpaces = anchor.VirtualSpaces; + + VirtualSnapshotPoint active = textView.Selection.ActivePoint; + int oldSelectionActiveIndex = BeforeTextBufferChangeUndoPrimitive.MapToData(map, active.Position); + int oldSelectionActiveVirtualSpaces = active.VirtualSpaces; + + TextSelectionMode oldSelectionMode = textView.Selection.Mode; + + if (oldCaretVirtualSpaces != 0 || + oldSelectionAnchorIndex != oldCaretIndex || + oldSelectionAnchorVirtualSpaces != 0 || + oldSelectionActiveIndex != oldCaretIndex || + oldSelectionActiveVirtualSpaces != 0 || + oldSelectionMode != TextSelectionMode.Stream) + { + return new GeneralBeforeTextBufferChangeUndoPrimitive + (undoHistory, oldCaretIndex, caret.Affinity, oldCaretVirtualSpaces, oldSelectionAnchorIndex, + oldSelectionAnchorVirtualSpaces, oldSelectionActiveIndex, oldSelectionActiveVirtualSpaces, oldSelectionMode); + } + else + { + return new BeforeTextBufferChangeUndoPrimitive(undoHistory, oldCaretIndex, caret.Affinity); + } + } + + //Get the map -- if any -- used to map points in the view's edit buffer to the data buffer. The map is needed because the undo history + //typically lives on the data buffer, but is used by the view on the edit buffer and a view (if any) on the data buffer. If there isn't + //a contract that guarantees that the contents of the edit and databuffers are the same, undoing an action on the edit buffer view and + //then undoing it on the data buffer view will cause cause the undo to try and restore caret/selection (in the data buffer) the coorinates + //saved in the edit buffer. This isn't good. + internal static IMapEditToData GetMap(ITextView view) + { + IMapEditToData map = null; + if (view.TextViewModel.EditBuffer != view.TextViewModel.DataBuffer) + { + view.Properties.TryGetProperty(typeof(IMapEditToData), out map); + } + + return map; + } + + //Map point from a position in the edit buffer to a position in the data buffer (== if there is no map, otherwise ask the map). + internal static int MapToData(IMapEditToData map, int point) + { + return (map != null) ? map.MapEditToData(point) : point; + } + + //Map point from a position in the data buffer to a position in the edit buffer (== if there is no map, otherwise ask the map). + internal static int MapToEdit(IMapEditToData map, int point) + { + return (map != null) ? map.MapDataToEdit(point) : point; + } + + protected BeforeTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, int caretIndex, PositionAffinity caretAffinity) + { + _undoHistory = undoHistory; + _oldCaretIndex = caretIndex; + _oldCaretAffinityByte = (byte)caretAffinity; + _canUndo = true; + } + + // Internal empty constructor for unit testing. + internal BeforeTextBufferChangeUndoPrimitive() { } + + /// <summary> + /// The <see cref="ITextView"/> that this <see cref="BeforeTextBufferChangeUndoPrimitive"/> is bound to. + /// </summary> + internal ITextView GetTextView() + { + ITextView view = null; + _undoHistory.Properties.TryGetProperty(typeof(ITextView), out view); + return view; + } + + #region UndoPrimitive Members + + /// <summary> + /// Returns true if operation can be undone, false otherwise. + /// </summary> + public override bool CanUndo + { + get { return _canUndo; } + } + + /// <summary> + /// Returns true if operation can be redone, false otherwise. + /// </summary> + public override bool CanRedo + { + get { return !_canUndo; } + } + + /// <summary> + /// Do the action. + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be redone.</exception> + public override void Do() + { + // Validate, we shouldn't be allowed to undo + if (!CanRedo) + { + throw new InvalidOperationException(Strings.CannotRedo); + } + + // Currently, no action is done on the redo. To set the caret and selection for after a TextBuffer change redo, there is the AfterTextBufferChangeUndoPrimitive. + // This Redo should not do anything with the caret and selection, because we only want to reset them after the TextBuffer change has occurred. + // Therefore, we need to add this UndoPrimitive to the undo stack before the UndoPrimitive for the TextBuffer change. On an undo, the TextBuffer changed UndoPrimitive + // will fire it's Undo first, and than the Undo for this UndoPrimitive will fire. + // However, on a redo, the Redo for this UndoPrimitive will be fired, and then the Redo for the TextBuffer change UndoPrimitive. If we had set any caret placement/selection here (ie the new caret placement/selection), + // we may crash because the TextBuffer change has not occurred yet (ie you try to set the caret to be at CharacterIndex 1 when the TextBuffer is still empty). + + _canUndo = true; + } + + /// <summary> + /// Undo the action. + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception> + public override void Undo() + { + // Validate that we can undo this change + if (!CanUndo) + { + throw new InvalidOperationException(Strings.CannotUndo); + } + + // Restore the old caret position and active selection + var view = this.GetTextView(); + Debug.Assert(view == null || !view.IsClosed, "Attempt to undo/redo on a closed view? This shouldn't happen."); + if (view != null && !view.IsClosed) + { + UndoMoveCaretAndSelect(view, BeforeTextBufferChangeUndoPrimitive.GetMap(view)); + view.Caret.EnsureVisible(); + } + + _canUndo = false; + } + + /// <summary> + /// Move the caret and restore the selection as part of the Undo operation. + /// </summary> + protected virtual void UndoMoveCaretAndSelect(ITextView view, IMapEditToData map) + { + SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, MapToEdit(map, _oldCaretIndex)); + + view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret), (PositionAffinity)_oldCaretAffinityByte); + view.Selection.Clear(); + } + + protected virtual int OldCaretVirtualSpaces + { + get { return 0; } + } + + public override bool CanMerge(ITextUndoPrimitive older) + { + if (older == null) + { + throw new ArgumentNullException("older"); + } + + AfterTextBufferChangeUndoPrimitive olderPrimitive = older as AfterTextBufferChangeUndoPrimitive; + // We can only merge with IUndoPrimitives of AfterTextBufferChangeUndoPrimitive type + if (olderPrimitive == null) + { + return false; + } + + return (olderPrimitive.CaretIndex == _oldCaretIndex) && (olderPrimitive.CaretVirtualSpace == OldCaretVirtualSpaces); + } + + #endregion + } + + /// <summary> + /// The UndoPrimitive to take place on the Undo stack before a text buffer change. This is the general + /// version of the primitive that handles all cases, including those involving selections and virtual space. + /// </summary> + internal class GeneralBeforeTextBufferChangeUndoPrimitive : BeforeTextBufferChangeUndoPrimitive + { + private int _oldCaretVirtualSpaces; + private int _oldSelectionAnchorIndex; + private int _oldSelectionAnchorVirtualSpaces; + private int _oldSelectionActiveIndex; + private int _oldSelectionActiveVirtualSpaces; + private TextSelectionMode _oldSelectionMode; + + public GeneralBeforeTextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, + int oldCaretIndex, + PositionAffinity oldCaretAffinity, + int oldCaretVirtualSpaces, + int oldSelectionAnchorIndex, + int oldSelectionAnchorVirtualSpaces, + int oldSelectionActiveIndex, + int oldSelectionActiveVirtualSpaces, + TextSelectionMode oldSelectionMode) + : base(undoHistory, oldCaretIndex, oldCaretAffinity) + { + _oldCaretVirtualSpaces = oldCaretVirtualSpaces; + _oldSelectionAnchorIndex = oldSelectionAnchorIndex; + _oldSelectionAnchorVirtualSpaces = oldSelectionAnchorVirtualSpaces; + _oldSelectionActiveIndex = oldSelectionActiveIndex; + _oldSelectionActiveVirtualSpaces = oldSelectionActiveVirtualSpaces; + _oldSelectionMode = oldSelectionMode; + } + + /// <summary> + /// Move the caret and restore the selection as part of the Undo operation. + /// </summary> + protected override void UndoMoveCaretAndSelect(ITextView view, IMapEditToData map) + { + SnapshotPoint newCaret = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldCaretIndex)); + SnapshotPoint newAnchor = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldSelectionAnchorIndex)); + SnapshotPoint newActive = new SnapshotPoint(view.TextSnapshot, BeforeTextBufferChangeUndoPrimitive.MapToEdit(map, _oldSelectionActiveIndex)); + + view.Caret.MoveTo(new VirtualSnapshotPoint(newCaret, _oldCaretVirtualSpaces), (PositionAffinity)_oldCaretAffinityByte); + + view.Selection.Mode = _oldSelectionMode; + + var virtualAnchor = new VirtualSnapshotPoint(newAnchor, _oldSelectionAnchorVirtualSpaces); + var virtualActive = new VirtualSnapshotPoint(newActive, _oldSelectionActiveVirtualSpaces); + + // Buffer may have been changed by one of the listeners on the caret move event. + virtualAnchor = virtualAnchor.TranslateTo(view.TextSnapshot); + virtualActive = virtualActive.TranslateTo(view.TextSnapshot); + + view.Selection.Select(virtualAnchor, virtualActive); + } + + protected override int OldCaretVirtualSpaces + { + get { return _oldCaretVirtualSpaces; } + } + } +} diff --git a/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs b/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs new file mode 100644 index 0000000..44fa09e --- /dev/null +++ b/src/Text/Impl/EditorOperations/CollapsedMoveUndoPrimitive.cs @@ -0,0 +1,235 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.Diagnostics; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Outlining; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + + /// <summary> + /// BeforeCollapsedMoveUndoPrimitive is added to the Undo stack before a collapsed region is moved. + /// Undo operations will cause this primitive to collapse the given regions, returning them + /// to their original pre-move state. Redo is ignored here and handled in the AfterCollapsedMoveUndoPrimitive. + /// </summary> + internal class BeforeCollapsedMoveUndoPrimitive : CollapsedMoveUndoPrimitive + { + public BeforeCollapsedMoveUndoPrimitive(IOutliningManager outliningManager, ITextView textView, IEnumerable<Tuple<Span, IOutliningRegionTag>> collapsedSpans) + : base(outliningManager, textView, collapsedSpans) + { + + } + + /// <summary> + /// Redo the action + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception> + public override void Do() + { + if (!CanRedo) + { + throw new InvalidOperationException(Strings.CannotRedo); + } + + // Redo is handled by AfterCollapsedMoveUndoPrimitive and there is nothing for the before to Do here. + _canUndo = true; + } + + /// <summary> + /// Undo the action. + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception> + public override void Undo() + { + // Validate that we can undo this change + if (!CanUndo) + { + throw new InvalidOperationException(Strings.CannotUndo); + } + + CollapseRegions(); + + _canUndo = false; + } + } + + + /// <summary> + /// AfterCollapsedMoveUndoPrimitive is added to the Undo stack after a collapsed region has been moved. + /// When a Redo occurs this primitive will collapse the regions to return them to their correct state. + /// Undo operations are ignored here as they are handled in the BeforeCollapsedMoveUndoPrimitive. + /// </summary> + internal class AfterCollapsedMoveUndoPrimitive : CollapsedMoveUndoPrimitive + { + public AfterCollapsedMoveUndoPrimitive(IOutliningManager outliningManager, ITextView textView, IEnumerable<Tuple<Span, IOutliningRegionTag>> collapsedSpans) + : base(outliningManager, textView, collapsedSpans) + { + + } + + /// <summary> + /// Redo the action + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception> + public override void Do() + { + if (!CanRedo) + { + throw new InvalidOperationException(Strings.CannotRedo); + } + + CollapseRegions(); + + _canUndo = true; + } + + /// <summary> + /// Undo the action. + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception> + public override void Undo() + { + // Validate that we can undo this change + if (!CanUndo) + { + throw new InvalidOperationException(Strings.CannotUndo); + } + + // Undo is handled by the BeforeCollapsedMoveUndoPrimitive and so there is nothing to do here. + _canUndo = false; + } + } + + /// <summary> + /// CollapsedMoveUndoPrimitive handles re-tagging of regions and collapsing them during moves. + /// OutliningUndoManager does not handle collapsing moved regions due to the way it listens to collapse/expands + /// events and then records an Expand undo operation for the collapse. For Move Line operations we need to always collapse. + /// Re-tagging of the outlined regions is needed since the tagging it not present on the newly inserted text + /// in the middle of the undo/redo operation, and without it the region cannot be collapsed. + /// </summary> + internal abstract class CollapsedMoveUndoPrimitive : TextUndoPrimitive + { + readonly IOutliningManager _outliningManager; + readonly ITextView _textView; + readonly IEnumerable<Tuple<Span, IOutliningRegionTag>> _collapsedSpans; + protected bool _canUndo; + + public CollapsedMoveUndoPrimitive(IOutliningManager outliningManager, ITextView textView, IEnumerable<Tuple<Span, IOutliningRegionTag>> collaspedSpans) + { + if (textView == null) + { + throw new ArgumentNullException("textView"); + } + + if (outliningManager == null) + { + throw new ArgumentNullException("outliningManager"); + } + + if (collaspedSpans == null) + { + throw new ArgumentNullException("collaspedSpans"); + } + + _outliningManager = outliningManager; + _textView = textView; + _collapsedSpans = collaspedSpans; + + _canUndo = true; + } + + // Re-collapse the spans + public void CollapseRegions() + { + if (_outliningManager == null || _textView == null || _collapsedSpans == null) + return; + + ITextSnapshot snapshot = _textView.TextBuffer.CurrentSnapshot; + + // Get a span that includes all collapsed regions + int min = Int32.MinValue; + int max = Int32.MinValue; + + foreach (Tuple<Span, IOutliningRegionTag> span in _collapsedSpans) + { + if (min == Int32.MinValue || span.Item1.Start < min) + { + min = span.Item1.Start; + } + + if (max == Int32.MinValue || span.Item1.End > max) + { + max = span.Item1.End; + } + } + + // avoid running if there were no spans + if (min == Int32.MinValue) + { + Debug.Fail("No spans"); + return; + } + + // span containing all collapsed regions + SnapshotSpan entireSpan = new SnapshotSpan(snapshot, min, max - min); + + // regions have not yet been tagged by the language service during the undo/redo and + // so we need to tag them again in order to do the collapse + SimpleTagger<IOutliningRegionTag> simpleTagger = + _textView.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>(() => new SimpleTagger<IOutliningRegionTag>(_textView.TextBuffer)); + + Debug.Assert(!simpleTagger.GetTaggedSpans(entireSpan).GetEnumerator().MoveNext(), + "The code is not expecting the regions to be tagged already. Verify that redundant tagging is not occurring."); + + List<Span> toCollapse = new List<Span>(); + + // tag the regions and add them to the list to be bulk collapsed + foreach (Tuple<Span, IOutliningRegionTag> span in _collapsedSpans) + { + ITrackingSpan tspan = snapshot.CreateTrackingSpan(span.Item1, SpanTrackingMode.EdgeExclusive); + simpleTagger.CreateTagSpan(tspan, span.Item2); + + toCollapse.Add(span.Item1); + } + + // Disable the OutliningUndoManager to avoid it adding our collapse to the undo stack as an expand + bool disableOutliningUndo = _textView.Options.IsOutliningUndoEnabled(); + + try + { + if (disableOutliningUndo) + { + _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, false); + } + + // Do the collapse + _outliningManager.CollapseAll(entireSpan, colSpan => (!colSpan.IsCollapsed && toCollapse.Contains(colSpan.Extent.GetSpan(snapshot)))); + } + finally + { + if (disableOutliningUndo) + { + _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, true); + } + } + } + + public override bool CanUndo + { + get { return _canUndo; } + } + + public override bool CanRedo + { + get { return !_canUndo; } + } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/EditorOperations/EditorOperations.cs b/src/Text/Impl/EditorOperations/EditorOperations.cs new file mode 100644 index 0000000..0178e7a --- /dev/null +++ b/src/Text/Impl/EditorOperations/EditorOperations.cs @@ -0,0 +1,4590 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.ComponentModel.Composition; + using System.Diagnostics; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Text.RegularExpressions; + using System.Threading; + using System.Windows; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + using Microsoft.VisualStudio.Text.Formatting; + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Outlining; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Language.Intellisense.Utilities; + + /// <summary> + /// Provides a default operations set on top of the text editor + /// </summary> + internal class EditorOperations : IEditorOperations3 + { + enum CaretMovementDirection + { + Previous = 0, + Next = 1, + } + + enum LetterCase + { + Uppercase = 0, + Lowercase = 1, + } + + enum SelectionUpdate + { + Preserve, + Reset, + ResetUnlessEmptyBox, + Ignore, + ClearVirtualSpace + }; + + #region Private Members + + ITextView _textView; + EditorOperationsFactoryService _factory; + ITextDocument _textDocument; + ITextStructureNavigator _textStructureNavigator; + ITextUndoHistory _undoHistory; + IViewPrimitives _editorPrimitives; + IEditorOptions _editorOptions; + + private ITrackingSpan _immProvisionalComposition; + + /// <summary> + /// A data format used to tag the contents of the clipboard so that it's clear + /// the data has been put in the clipboard by our editor + /// </summary> + private const string _clipboardLineBasedCutCopyTag = "VisualStudioEditorOperationsLineCutCopyClipboardTag"; + + /// <summary> + /// A data format used to tag the contents of the clipboard as a box selection. + /// This is the same string that was used in VS9 and previous versions. + /// </summary> + private const string _boxSelectionCutCopyTag = "MSDEVColumnSelect"; + + #endregion // Private Members + + /// <summary> + /// Constructs an <see cref="EditorOperations"/> bound to a given <see cref="ITextView"/>. + /// </summary> + /// <param name="textView"> + /// The text editor to which this operations provider should bind to + /// </param> + internal EditorOperations(ITextView textView, + EditorOperationsFactoryService factory) + { + // Validate + if (textView == null) + throw new ArgumentNullException("textView"); + if (factory == null) + throw new ArgumentNullException("factory"); + + _textView = textView; + _factory = factory; + + _editorPrimitives = factory.EditorPrimitivesProvider.GetViewPrimitives(textView); + // Get the TextStructure Navigator + _textStructureNavigator = factory.TextStructureNavigatorFactory.GetTextStructureNavigator(_textView.TextBuffer); + Debug.Assert(_textStructureNavigator != null); + + _undoHistory = factory.UndoHistoryRegistry.RegisterHistory(_textView.TextBuffer); + // Ensure that there is an ITextBufferUndoManager created for our TextBuffer + ITextBufferUndoManager textBufferUndoManager = factory.TextBufferUndoManagerProvider.GetTextBufferUndoManager(_textView.TextBuffer); + Debug.Assert(textBufferUndoManager != null); + + _editorOptions = factory.EditorOptionsProvider.GetOptions(textView); + + _factory.TextDocumentFactoryService.TryGetTextDocument(_textView.TextDataModel.DocumentBuffer, out _textDocument); + } + + + #region IEditorOperations2 Members + + public bool MoveSelectedLinesUp() + { + Func<bool> action = () => + { + bool success = false; + + IWpfTextView view = _textView as IWpfTextView; + + // find line start + IWpfTextViewLine startViewLine = GetLineStart(view, view.Selection.Start.Position); + SnapshotPoint start = startViewLine.Start; + ITextSnapshotLine startLine = start.GetContainingLine(); + + // find the last line view + IWpfTextViewLine endViewLine = GetLineEnd(view, view.Selection.End.Position); + SnapshotPoint end = endViewLine.EndIncludingLineBreak; + ITextSnapshotLine endLine = endViewLine.End.GetContainingLine(); + + ITextSnapshot snapshot = endLine.Snapshot; + + // Handle the case where multiple lines are selected and the caret is sitting just after the line break on the next line. + // Shortening the selection here handles the case where the last line is a collapsed region. Using endLine.End will give + // a line within the collapsed region instead of skipping it all together. + if (GetLineEnd(view, startViewLine.Start) != endViewLine + && view.Selection.End.Position == GetLineStart(view, view.Selection.End.Position).Start + && !view.Selection.End.IsInVirtualSpace) + { + endLine = snapshot.GetLineFromLineNumber(endLine.LineNumber - 1); + end = endLine.EndIncludingLineBreak; + endViewLine = view.GetTextViewLineContainingBufferPosition(view.Selection.End.Position - 1); + } + + #region Initial Asserts + + Debug.Assert(view.Selection.Start.Position.Snapshot == view.TextSnapshot, "Selection is out of sync with view."); + + Debug.Assert(view.TextSnapshot == view.TextBuffer.CurrentSnapshot, "View is out of sync with text buffer."); + + Debug.Assert(view.TextSnapshot == snapshot, "Text view lines are out of sync with the view"); + + #endregion + + // check if we are at the top of the file, or trying to move a blank line + if (startLine.LineNumber < 1 || start == end) + { + // noop + success = true; + } + else + { + // find the line we are going to move over + ITextSnapshotLine prevLine = snapshot.GetLineFromLineNumber(startLine.LineNumber - 1); + + // prevLineExtent is different from prevLine.Extent and avoids issues around collapsed regions + SnapshotPoint prevLineStart = GetLineStart(view, prevLine.Start).Start; + SnapshotSpan prevLineExtent = new SnapshotSpan(prevLineStart, prevLine.End); + SnapshotSpan prevLineExtentIncludingLineBreak = new SnapshotSpan(prevLineStart, prevLine.EndIncludingLineBreak); + + using (ITextEdit edit = view.TextBuffer.CreateEdit()) + { + int offset; + + SnapshotSpan curLineExtent = new SnapshotSpan(startViewLine.Start, endViewLine.End); + SnapshotSpan curLineExtentIncLineBreak = new SnapshotSpan(startViewLine.Start, endViewLine.EndIncludingLineBreak); + string curLineText = curLineExtentIncLineBreak.GetText(); + + List<Tuple<Span, IOutliningRegionTag>> collapsedSpansInCurLine = null; + bool hasCollapsedRegions = false; + + IOutliningManager outliningManager = (_factory.OutliningManagerService != null) + ? _factory.OutliningManagerService.GetOutliningManager(view) + : null; + + if (outliningManager != null) + { + collapsedSpansInCurLine = outliningManager.GetCollapsedRegions(new NormalizedSnapshotSpanCollection(curLineExtent)) + .Select(collapsed => Tuple.Create(collapsed.Extent.GetSpan(curLineExtent.Snapshot).Span, collapsed.Tag)).ToList(); + + hasCollapsedRegions = collapsedSpansInCurLine.Count > 0; + + // check if we have collapsed spans in the selection and add the undo primitive if so + if (hasCollapsedRegions) + { + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesUp)) + { + BeforeCollapsedMoveUndoPrimitive undoPrim = new BeforeCollapsedMoveUndoPrimitive(outliningManager, view, collapsedSpansInCurLine); + undoTransaction.AddUndo(undoPrim); + undoTransaction.Complete(); + } + } + } + + string nextLineText = prevLineExtentIncludingLineBreak.GetText(); + + offset = nextLineText.Length; + + // make the change + edit.Delete(curLineExtentIncLineBreak); + edit.Insert(prevLineExtentIncludingLineBreak.Start, curLineText); + + // swap the line break around if needed for the last line of the selection + if (endViewLine.LineBreakLength == 0 && endViewLine.EndIncludingLineBreak == snapshot.Length) + { + // put the line break on the line we just moved that didn't have one + edit.Insert(prevLine.ExtentIncludingLineBreak.Start, prevLine.GetLineBreakText()); + + // delete the break from the line now at the end of the file + edit.Delete(new SnapshotSpan(prevLine.End, prevLine.EndIncludingLineBreak)); + } + + + if (!edit.HasFailedChanges) + { + // store the position before the edit is applied + int anchorPos = view.Selection.AnchorPoint.Position.Position; + int anchorVirtualSpace = view.Selection.AnchorPoint.VirtualSpaces; + int activePos = view.Selection.ActivePoint.Position.Position; + int activeVirtualSpace = view.Selection.ActivePoint.VirtualSpaces; + var selectionMode = view.Selection.Mode; + + // apply the edit + ITextSnapshot newSnapshot = edit.Apply(); + + if (newSnapshot != snapshot) + { + + // Update the selection and caret position after the move + ITextSnapshot currentSnapshot = snapshot.TextBuffer.CurrentSnapshot; + VirtualSnapshotPoint desiredAnchor = new VirtualSnapshotPoint( + new SnapshotPoint(newSnapshot, Math.Min(anchorPos - offset, newSnapshot.Length)), anchorVirtualSpace) + .TranslateTo(currentSnapshot, PointTrackingMode.Negative); + VirtualSnapshotPoint desiredActive = new VirtualSnapshotPoint( + new SnapshotPoint(newSnapshot, Math.Min(activePos - offset, newSnapshot.Length)), activeVirtualSpace) + .TranslateTo(currentSnapshot, PointTrackingMode.Negative); + + // Keep the selection and caret position the same + SelectAndMoveCaret(desiredAnchor, desiredActive, selectionMode, EnsureSpanVisibleOptions.None); + + // Recollapse the spans + if (outliningManager != null && hasCollapsedRegions) + { + // This comes from adhocoutliner.cs in env\editor\pkg\impl\outlining and will not be available outside of VS + SimpleTagger<IOutliningRegionTag> simpleTagger = + view.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>( + () => new SimpleTagger<IOutliningRegionTag>(view.TextBuffer)); + + if (simpleTagger != null) + { + if (hasCollapsedRegions) + { + List<Tuple<ITrackingSpan, IOutliningRegionTag>> addedSpans = collapsedSpansInCurLine.Select(tuple => Tuple.Create(newSnapshot.CreateTrackingSpan(tuple.Item1.Start - offset, tuple.Item1.Length, + SpanTrackingMode.EdgeExclusive), tuple.Item2)).ToList(); + + if (addedSpans.Count > 0) + { + List<Tuple<Span, IOutliningRegionTag>> spansForUndo = new List<Tuple<Span, IOutliningRegionTag>>(); + + foreach (var addedSpan in addedSpans) + { + simpleTagger.CreateTagSpan(addedSpan.Item1, addedSpan.Item2); + spansForUndo.Add(new Tuple<Span, IOutliningRegionTag>(addedSpan.Item1.GetSpan(newSnapshot), addedSpan.Item2)); + } + + SnapshotSpan changedSpan = new SnapshotSpan(addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot).Start).Min(), + addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot).End).Max()); + + List<SnapshotSpan> addedSnapshotSpans = addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot)).ToList(); + + + bool disableOutliningUndo = _editorOptions.IsOutliningUndoEnabled(); + + // Recollapse the spans + // We need to disable the OutliningUndoManager for this operation otherwise an undo will expand it + try + { + if (disableOutliningUndo) + { + _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, false); + } + + outliningManager.CollapseAll(changedSpan, collapsible => addedSnapshotSpans.Contains(collapsible.Extent.GetSpan(newSnapshot))); + } + finally + { + if (disableOutliningUndo) + { + _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, true); + } + } + + // we need to recollapse after a redo + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesUp)) + { + AfterCollapsedMoveUndoPrimitive undoPrim = new AfterCollapsedMoveUndoPrimitive(outliningManager, view, spansForUndo); + undoTransaction.AddUndo(undoPrim); + undoTransaction.Complete(); + } + } + } + } + } + + success = true; + } + } + } + } + + return success; + }; + + return ExecuteAction(Strings.MoveSelLinesUp, action, SelectionUpdate.Ignore, true); + } + + public bool MoveSelectedLinesDown() + { + Func<bool> action = () => + { + + bool success = false; + + IWpfTextView view = _textView as IWpfTextView; + + // find line start + IWpfTextViewLine startViewLine = GetLineStart(view, view.Selection.Start.Position); + SnapshotPoint start = startViewLine.Start; + ITextSnapshotLine startLine = start.GetContainingLine(); + + // find the last line view + IWpfTextViewLine endViewLine = GetLineEnd(view, view.Selection.End.Position); + ITextSnapshotLine endLine = endViewLine.End.GetContainingLine(); + + ITextSnapshot snapshot = endLine.Snapshot; + + // Handle the case where multiple lines are selected and the caret is sitting just after the line break on the next line. + // Shortening the selection here handles the case where the last line is a collapsed region. Using endLine.End will give + // a line within the collapsed region instead of skipping it all together. + if (GetLineEnd(view, startViewLine.Start) != endViewLine + && view.Selection.End.Position == GetLineStart(view, view.Selection.End.Position).Start + && !view.Selection.End.IsInVirtualSpace) + { + endLine = snapshot.GetLineFromLineNumber(endLine.LineNumber - 1); + endViewLine = view.GetTextViewLineContainingBufferPosition(view.Selection.End.Position - 1); + } + + #region Initial Asserts + + Debug.Assert(view.Selection.Start.Position.Snapshot == view.TextSnapshot, "Selection is out of sync with view."); + + Debug.Assert(view.TextSnapshot == view.TextBuffer.CurrentSnapshot, "View is out of sync with text buffer."); + + Debug.Assert(view.TextSnapshot == snapshot, "Text view lines are out of sync with the view"); + + #endregion + + // check if we are at the end of the file + if ((endLine.LineNumber + 1) >= snapshot.LineCount) + { + // noop + success = true; + } + else + { + // nextLineExtent is different from prevLine.Extent and avoids issues around collapsed regions + IWpfTextViewLine lastNextLine = GetLineEnd(view, endViewLine.EndIncludingLineBreak); + SnapshotSpan nextLineExtent = new SnapshotSpan(endViewLine.EndIncludingLineBreak, lastNextLine.End); + SnapshotSpan nextLineExtentIncludingLineBreak = new SnapshotSpan(endViewLine.EndIncludingLineBreak, lastNextLine.EndIncludingLineBreak); + + using (ITextEdit edit = view.TextBuffer.CreateEdit()) + { + SnapshotSpan curLineExtent = new SnapshotSpan(startViewLine.Start, endViewLine.End); + SnapshotSpan curLineExtentIncLineBreak = new SnapshotSpan(startViewLine.Start, endViewLine.EndIncludingLineBreak); + string curLineText = curLineExtentIncLineBreak.GetText(); + + string nextLineText = nextLineExtentIncludingLineBreak.GetText(); + + if (nextLineText.Length == 0) + { + // end of file - noop + success = true; + } + else + { + List<Tuple<Span, IOutliningRegionTag>> collapsedSpansInCurLine = null; + bool hasCollapsedRegions = false; + + IOutliningManager outliningManager = (_factory.OutliningManagerService != null) + ? _factory.OutliningManagerService.GetOutliningManager(view) + : null; + + if (outliningManager != null) + { + collapsedSpansInCurLine = outliningManager.GetCollapsedRegions(new NormalizedSnapshotSpanCollection(curLineExtent)) + .Select(collapsed => Tuple.Create(collapsed.Extent.GetSpan(curLineExtent.Snapshot).Span, collapsed.Tag)).ToList(); + + hasCollapsedRegions = collapsedSpansInCurLine.Count > 0; + + // check if we have collapsed spans in the selection and add the undo primitive if so + if (hasCollapsedRegions) + { + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesDown)) + { + BeforeCollapsedMoveUndoPrimitive undoPrim = new BeforeCollapsedMoveUndoPrimitive(outliningManager, view, collapsedSpansInCurLine); + undoTransaction.AddUndo(undoPrim); + undoTransaction.Complete(); + } + } + } + + + int offset = nextLineText.Length; + + // a line without a line break + if (nextLineExtent == nextLineExtentIncludingLineBreak) + { + string lineBreakText = new SnapshotSpan(startLine.End, startLine.EndIncludingLineBreak).GetText(); + + offset += lineBreakText.Length; + + curLineText = lineBreakText + curLineText.Substring(0, curLineText.Length - lineBreakText.Length); + } + + + edit.Delete(curLineExtentIncLineBreak); + edit.Insert(nextLineExtentIncludingLineBreak.End, curLineText); + + if (edit.HasFailedChanges) + { + success = false; + } + else + { + int anchorPos = view.Selection.AnchorPoint.Position.Position; + int anchorVirtualSpace = view.Selection.AnchorPoint.VirtualSpaces; + int activePos = view.Selection.ActivePoint.Position.Position; + int activeVirtualSpace = view.Selection.ActivePoint.VirtualSpaces; + var selectionMode = view.Selection.Mode; + + ITextSnapshot newSnapshot = edit.Apply(); + if (newSnapshot == snapshot) + { + success = false; + } + else + { + // Update the selection and caret position after the move + ITextSnapshot currentSnapshot = snapshot.TextBuffer.CurrentSnapshot; + VirtualSnapshotPoint desiredAnchor = new VirtualSnapshotPoint(new SnapshotPoint(newSnapshot, Math.Min(anchorPos + offset, newSnapshot.Length)), + anchorVirtualSpace).TranslateTo(currentSnapshot, PointTrackingMode.Negative); + VirtualSnapshotPoint desiredActive = new VirtualSnapshotPoint(new SnapshotPoint(newSnapshot, Math.Min(activePos + offset, newSnapshot.Length)), + activeVirtualSpace).TranslateTo(currentSnapshot, PointTrackingMode.Negative); + + // keep the caret position and selection after the move + SelectAndMoveCaret(desiredAnchor, desiredActive, selectionMode, EnsureSpanVisibleOptions.None); + + // Recollapse the spans + if (outliningManager != null && hasCollapsedRegions) + { + // This comes from adhocoutliner.cs in env\editor\pkg\impl\outlining and will not be available outside of VS + SimpleTagger<IOutliningRegionTag> simpleTagger = + view.TextBuffer.Properties.GetOrCreateSingletonProperty<SimpleTagger<IOutliningRegionTag>>(() => new SimpleTagger<IOutliningRegionTag>(view.TextBuffer)); + + if (simpleTagger != null) + { + if (hasCollapsedRegions) + { + List<Tuple<ITrackingSpan, IOutliningRegionTag>> addedSpans = collapsedSpansInCurLine.Select(tuple => Tuple.Create(newSnapshot.CreateTrackingSpan(tuple.Item1.Start + offset, + tuple.Item1.Length, SpanTrackingMode.EdgeExclusive), tuple.Item2)).ToList(); + + if (addedSpans.Count > 0) + { + List<Tuple<Span, IOutliningRegionTag>> spansForUndo = new List<Tuple<Span, IOutliningRegionTag>>(); + + // add spans to tracking + foreach (var addedSpan in addedSpans) + { + simpleTagger.CreateTagSpan(addedSpan.Item1, addedSpan.Item2); + spansForUndo.Add(new Tuple<Span, IOutliningRegionTag>(addedSpan.Item1.GetSpan(newSnapshot), addedSpan.Item2)); + } + + SnapshotSpan changedSpan = new SnapshotSpan(addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot).Start).Min(), + addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot).End).Max()); + + List<SnapshotSpan> addedSnapshotSpans = addedSpans.Select(tuple => tuple.Item1.GetSpan(newSnapshot)).ToList(); + + bool disableOutliningUndo = _editorOptions.IsOutliningUndoEnabled(); + + // Recollapse the span + // We need to disable the OutliningUndoManager for this operation otherwise an undo will expand it + try + { + if (disableOutliningUndo) + { + _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, false); + } + + outliningManager.CollapseAll(changedSpan, collapsible => addedSnapshotSpans.Contains(collapsible.Extent.GetSpan(newSnapshot))); + } + finally + { + if (disableOutliningUndo) + { + _textView.Options.SetOptionValue(DefaultTextViewOptions.OutliningUndoOptionId, true); + } + } + + // we need to recollapse after a redo + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.MoveSelLinesDown)) + { + AfterCollapsedMoveUndoPrimitive undoPrim = new AfterCollapsedMoveUndoPrimitive(outliningManager, view, spansForUndo); + undoTransaction.AddUndo(undoPrim); + undoTransaction.Complete(); + } + } + } + } + } + + success = true; + } + } + } + } + } + return success; + }; + + return ExecuteAction(Strings.MoveSelLinesDown, action, SelectionUpdate.Ignore, true); + } + + private static IWpfTextViewLine GetLineStart(IWpfTextView view, SnapshotPoint snapshotPoint) + { + IWpfTextViewLine line = view.GetTextViewLineContainingBufferPosition(snapshotPoint); + while (!line.IsFirstTextViewLineForSnapshotLine) + { + line = view.GetTextViewLineContainingBufferPosition(line.Start - 1); + } + return line; + } + + private static IWpfTextViewLine GetLineEnd(IWpfTextView view, SnapshotPoint snapshotPoint) + { + IWpfTextViewLine line = view.GetTextViewLineContainingBufferPosition(snapshotPoint); + while (!line.IsLastTextViewLineForSnapshotLine) + { + line = view.GetTextViewLineContainingBufferPosition(line.EndIncludingLineBreak); + } + return line; + } + + #endregion + + + #region IEditorOperations Members + + public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint) + { + SelectAndMoveCaret(anchorPoint, activePoint, TextSelectionMode.Stream, EnsureSpanVisibleOptions.MinimumScroll); + } + + public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint, TextSelectionMode selectionMode) + { + this.SelectAndMoveCaret(anchorPoint, activePoint, selectionMode, EnsureSpanVisibleOptions.MinimumScroll); + } + + public void SelectAndMoveCaret(VirtualSnapshotPoint anchorPoint, VirtualSnapshotPoint activePoint, TextSelectionMode selectionMode, EnsureSpanVisibleOptions? scrollOptions) + { + bool empty = (anchorPoint == activePoint); + + // TODO: Whenever caret/selection is updated to offer a way to set both simultaneously without either eventing before + // the other is updated, we should update this method to use that. There are potential bugs below in how clients + // react to things like selection moving. For example, if someone reacts to moving the selection by moving the caret, + // the logic below will override that caret position, which may not be desirable. + + // The order of operations here is important: + // 1) We need to move the selection first. Clients (like VB) who listen for caret change need the selection to be correct, + // and we have yet to have clients that require the opposite order. See Dev10 #793198 for what happens when we do this selection-first. + // + // 2) Then we move the caret. This behaves differently, depending on if the new selection is empty or not (explained below). + + if (empty) + { + _textView.Selection.Clear(); + _textView.Selection.Mode = selectionMode; + + // Since the selection is empty, move the caret to the provided active point and translate that point + // to the view's text snapshot (in case someone was listening to the selection changed event and made a text edit). + // The empty selection will track the caret. + // See Dev10 #785792 for an example of what happens when we get this wrong by moving the caret to the active point + // of the selection when the selection is being cleared. + _textView.Caret.MoveTo(activePoint.TranslateTo(_textView.TextSnapshot)); + } + else + { + _textView.Selection.Select(anchorPoint, activePoint); + _textView.Selection.Mode = selectionMode; + + // Move the caret to the active point of the selection (don't use activePoint since someone -- on the selection changed event -- might have + // moved the selection). + // But if the selection is empty (it shouldn't be since anchorPoint != activePoint, but those points could be normalized to an empty span + // or someone could have moved it), move the caret to the requested activePoint. + _textView.Caret.MoveTo(_textView.Selection.IsEmpty + ? activePoint.TranslateTo(_textView.TextSnapshot) + : _textView.Selection.ActivePoint); + } + + // 3) If scrollOptions were provided, we're going to try and make the span visible using the provided options. + if (scrollOptions.HasValue) + { + //Make sure scrollOptions forces EnsureSpanVisible to bring the start or end of the selection into view appropriately. + if (_textView.Selection.IsReversed) + { + scrollOptions = scrollOptions.Value | EnsureSpanVisibleOptions.ShowStart; + } + else + { + scrollOptions = scrollOptions.Value & (~EnsureSpanVisibleOptions.ShowStart); + } + + // Try to make the span visible. Since we are setting the scrollOptions above, this will ensure the caret + // is visible as well (we do not need to worry about the case where the caret is at the end of a word-wrapped + // line since -- when the caret is moved to a VirtualSnapshotPoint -- it won't be). + _textView.ViewScroller.EnsureSpanVisible(_textView.Selection.StreamSelectionSpan, scrollOptions.Value); + } + } + + /// <summary> + /// Moves one character to the right. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void MoveToNextCharacter(bool select) + { + _editorPrimitives.Caret.MoveToNextCharacter(select); + } + + /// <summary> + /// Moves one character to the left. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void MoveToPreviousCharacter(bool select) + { + bool isCaretAtStartOfViewLine = (!_textView.Caret.InVirtualSpace) && + (_textView.Caret.Position.BufferPosition == _textView.Caret.ContainingTextViewLine.Start); + + //Prevent the caret from moving from column 0 to the end of the previous line if either: + // virtual space is turned on or + // the user is extending a box selection. + if (isCaretAtStartOfViewLine && (_editorOptions.IsVirtualSpaceEnabled() || (select && (_textView.Selection.Mode == TextSelectionMode.Box)))) + { + return; + } + + _editorPrimitives.Caret.MoveToPreviousCharacter(select); + } + + /// <summary> + /// Moves the caret to the next word. + /// </summary> + /// <param name="select"> + /// Specifies whether or not selection is extended as the caret is moved. + /// </param> + public void MoveToNextWord(bool select) + { + _editorPrimitives.Caret.MoveToNextWord(select); + } + + /// <summary> + /// Moves the caret to the previous word. + /// </summary> + /// <param name="select"> + /// Specifies whether or not selection is extended as the caret is moved. + /// </param> + public void MoveToPreviousWord(bool select) + { + // In extending a box selection, we don't want this to jump to the previous line (if + // we are on the beginning of a line) + if (select && _textView.Selection.Mode == TextSelectionMode.Box && !_textView.Caret.InVirtualSpace) + { + if (_editorPrimitives.Caret.CurrentPosition == _editorPrimitives.Caret.StartOfViewLine) + return; + } + + _editorPrimitives.Caret.MoveToPreviousWord(select); + } + + /// <summary> + /// Sets the caret at the start of the document. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void MoveToStartOfDocument(bool select) + { + _editorPrimitives.Caret.MoveToStartOfDocument(select); + } + + /// <summary> + /// Sets the caret at the end of the document. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void MoveToEndOfDocument(bool select) + { + _editorPrimitives.Caret.MoveToEndOfDocument(select); + } + + /// <summary> + /// Moves the current line to the top of the view. + /// </summary> + public void MoveCurrentLineToTop() + { + _editorPrimitives.View.MoveLineToTop(_editorPrimitives.Caret.LineNumber); + } + + /// <summary> + /// Moves the current line to the bottom of the view. + /// </summary> + public void MoveCurrentLineToBottom() + { + _editorPrimitives.View.MoveLineToBottom(_editorPrimitives.Caret.LineNumber); + } + + public void MoveToStartOfLineAfterWhiteSpace(bool select) + { + int firstTextColumn = _editorPrimitives.Caret.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition; + if (firstTextColumn == _editorPrimitives.Caret.EndOfViewLine) + firstTextColumn = _editorPrimitives.Caret.StartOfViewLine; + + _editorPrimitives.Caret.MoveTo(firstTextColumn, select); + } + + public void MoveToStartOfNextLineAfterWhiteSpace(bool select) + { + DisplayTextPoint caretPoint = _editorPrimitives.Caret.Clone(); + caretPoint.MoveToBeginningOfNextLine(); + + int firstTextColumn = caretPoint.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition; + if (firstTextColumn == caretPoint.EndOfViewLine) + firstTextColumn = caretPoint.StartOfViewLine; + + _editorPrimitives.Caret.MoveTo(firstTextColumn, select); + } + + public void MoveToStartOfPreviousLineAfterWhiteSpace(bool select) + { + DisplayTextPoint caretPoint = _editorPrimitives.Caret.Clone(); + caretPoint.MoveToBeginningOfPreviousLine(); + + int firstTextColumn = caretPoint.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition; + if (firstTextColumn == caretPoint.EndOfViewLine) + firstTextColumn = caretPoint.StartOfViewLine; + + _editorPrimitives.Caret.MoveTo(firstTextColumn, select); + } + + public void MoveToLastNonWhiteSpaceCharacter(bool select) + { + int lastNonWhiteSpaceCharacterInLine = _editorPrimitives.Caret.EndOfViewLine - 1; + for (; lastNonWhiteSpaceCharacterInLine >= _editorPrimitives.Caret.StartOfViewLine; lastNonWhiteSpaceCharacterInLine--) + { + string nextCharacter = _editorPrimitives.View.GetTextPoint(lastNonWhiteSpaceCharacterInLine).GetNextCharacter(); + if (!char.IsWhiteSpace(nextCharacter[0])) + { + break; + } + } + + lastNonWhiteSpaceCharacterInLine = Math.Max(lastNonWhiteSpaceCharacterInLine, _editorPrimitives.Caret.StartOfLine); + + if (lastNonWhiteSpaceCharacterInLine != _editorPrimitives.Caret.CurrentPosition) + { + _editorPrimitives.Caret.MoveTo(lastNonWhiteSpaceCharacterInLine, select); + } + } + + public void MoveToTopOfView(bool select) + { + // TextViewLines may have both a partially and a fully hidden line at the top, or either, or neither. + ITextViewLineCollection lines = _editorPrimitives.View.AdvancedTextView.TextViewLines; + ITextViewLine firstVisibleLine = lines.FirstVisibleLine; + int iFirstVisibleLine = lines.GetIndexOfTextLine(firstVisibleLine); + ITextViewLine fullyVisibleLine = FindFullyVisibleLine(firstVisibleLine, iFirstVisibleLine + 1); + + MoveCaretToTextLine(fullyVisibleLine, select); + } + + public void MoveToBottomOfView(bool select) + { + // TextViewLines may have both a partially and a fully hidden line at the end, or either, or neither. + ITextViewLineCollection lines = _editorPrimitives.View.AdvancedTextView.TextViewLines; + ITextViewLine lastVisibleLine = lines.LastVisibleLine; + int iLastVisibleLine = lines.GetIndexOfTextLine(lastVisibleLine); + ITextViewLine fullyVisibleLine = FindFullyVisibleLine(lastVisibleLine, iLastVisibleLine - 1); + + MoveCaretToTextLine(fullyVisibleLine, select); + } + + /// <summary> + /// Deletes the word to the right of the current caret position. + /// </summary> + public bool DeleteWordToRight() + { + Func<bool> action = () => + { + TextPoint startPointOfDelete = _editorPrimitives.Caret.Clone(); + + TextRange nextWord = startPointOfDelete.GetNextWord(); + TextPoint endPointOfDelete = nextWord.GetStartPoint(); + + // If this delete did not start at the end of the line + // then only delete to the end of the line. + int endOfLine = startPointOfDelete.EndOfLine; + if (startPointOfDelete.CurrentPosition != endOfLine) + { + // It is possible that the text structure navigator has returned + // a word that spans multiple lines. In that case, just delete to + // the end of the line to match VS9 behavior. + if (endPointOfDelete.CurrentPosition > endOfLine) + { + endPointOfDelete.MoveTo(endOfLine); + } + else if (startPointOfDelete.CurrentPosition >= endPointOfDelete.CurrentPosition) + { + //If the startPointOfDelete was on the last word of the line then endPointOfDelete might be the + //start of the word so we should, instead, delete to the end of the line (or, at least, that is + //what we think is happening). + endPointOfDelete.MoveTo(endOfLine); + } + } + + return ExpandRangeToIncludeSelection(startPointOfDelete.GetTextRange(endPointOfDelete)).Delete(); + }; + + return ExecuteAction(Strings.DeleteWordToRight, action); + } + + /// <summary> + /// Deletes the word to the left of the current caret position. + /// </summary> + public bool DeleteWordToLeft() + { + Func<bool> action = () => + { + TextRange currentWord = _editorPrimitives.Caret.GetCurrentWord(); + TextRange rangeToDelete = currentWord; + if (_editorPrimitives.Caret.CurrentPosition > currentWord.GetStartPoint().CurrentPosition) + { + rangeToDelete = currentWord.GetStartPoint().GetTextRange(_editorPrimitives.Caret); + } + else + { + TextRange previousWord = _editorPrimitives.Caret.GetPreviousWord(); + rangeToDelete = previousWord.GetStartPoint().GetTextRange(_editorPrimitives.Caret); + } + + return ExpandRangeToIncludeSelection(rangeToDelete).Delete(); + }; + + return ExecuteAction(Strings.DeleteWordToLeft, action); + } + + public bool DeleteToBeginningOfLine() + { + Func<bool> action = () => + { + TextRange selectionRange = _editorPrimitives.Selection.Clone(); + + if (_editorPrimitives.Selection.IsReversed || _editorPrimitives.Selection.IsEmpty) + { + selectionRange.SetStart(_editorPrimitives.View.GetTextPoint(_editorPrimitives.Caret.StartOfLine)); + } + + return selectionRange.Delete(); + }; + + return ExecuteAction(Strings.DeleteToBOL, action); + } + + public bool DeleteToEndOfLine() + { + Func<bool> action = () => + { + TextRange selectionRange = _editorPrimitives.Selection.Clone(); + + if (!_editorPrimitives.Selection.IsReversed || _editorPrimitives.Selection.IsEmpty) + { + selectionRange.SetEnd(_editorPrimitives.View.GetTextPoint(_editorPrimitives.Caret.EndOfViewLine)); + } + + return selectionRange.Delete(); + }; + + return ExecuteAction(Strings.DeleteToEOL, action); + } + + /// <summary> + /// Deletes a character to the left of the current caret. + /// </summary> + public bool Backspace() + { + bool emptyBox = IsEmptyBoxSelection(); + NormalizedSnapshotSpanCollection boxDeletions = null; + + // First, handle cases that don't require edits + if (_textView.Selection.IsEmpty) + { + if (_textView.Caret.InVirtualSpace) + { + this.MoveCaretToPreviousIndentStopInVirtualSpace(); + + _textView.Caret.EnsureVisible(); + return true; + } + if (_textView.Caret.Position.BufferPosition.Position == 0) + { + return true; + } + } + // If the entire selection is in virtual space, clear it + else if (_textView.Selection.VirtualSelectedSpans.All(s => s.SnapshotSpan.IsEmpty && s.IsInVirtualSpace)) + { + this.ResetVirtualSelection(); + _textView.Caret.EnsureVisible(); + return true; + } + else if (emptyBox) // empty box selection, make sure it is valid + { + List<SnapshotSpan> spans = new List<SnapshotSpan>(); + + foreach (var span in _textView.Selection.VirtualSelectedSpans.Where(s => !s.IsInVirtualSpace).Select(s => s.SnapshotSpan)) + { + var line = span.Start.GetContainingLine(); + if (span.Start > line.Start) + { + spans.Add(_textView.GetTextElementSpan(span.Start - 1)); + } + } + + // If there is nothing to delete, clear the selection + if (spans.Count == 0) + { + _textView.Caret.MoveTo(_textView.Selection.Start); + _textView.Selection.Clear(); + _textView.Caret.EnsureVisible(); + return true; + } + + boxDeletions = new NormalizedSnapshotSpanCollection(spans); + } + + // Now, handle cases that require edits + Func<bool> action = () => + { + // 1. An empty selection mean backspace the caret + if (_textView.Selection.IsEmpty) + return _editorPrimitives.Caret.DeletePrevious(); + + // 2. If this is an empty box, we may need to capture the new active/anchor points, as points in virtual space + // won't track as we want them to through the edit. + VirtualSnapshotPoint? anchorPoint = null; + VirtualSnapshotPoint? activePoint = null; + + if (emptyBox) + { + if (_textView.Selection.AnchorPoint.IsInVirtualSpace) + { + anchorPoint = new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position, _textView.Selection.AnchorPoint.VirtualSpaces - 1); + } + if (_textView.Selection.ActivePoint.IsInVirtualSpace) + { + activePoint = new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position, _textView.Selection.ActivePoint.VirtualSpaces - 1); + } + } + + // 3. The selection is non-empty, so delete the selected spans (unless this is an empty box selection: An empty box selection means treat this as a backspace on each line) + NormalizedSnapshotSpanCollection deletion = boxDeletions ?? _textView.Selection.SelectedSpans; + + int selectionStartVirtualSpaces = _textView.Selection.Start.VirtualSpaces; + + if (!DeleteHelper(deletion)) + return false; + + // 5. Now, fix up the start and end points if this is an empty box + if (emptyBox && (anchorPoint.HasValue || activePoint.HasValue)) + { + VirtualSnapshotPoint newAnchor = (anchorPoint.HasValue) ? anchorPoint.Value.TranslateTo(_textView.TextSnapshot) : _textView.Selection.AnchorPoint; + VirtualSnapshotPoint newActive = (activePoint.HasValue) ? activePoint.Value.TranslateTo(_textView.TextSnapshot) : _textView.Selection.ActivePoint; + + _textView.Caret.MoveTo(_textView.Selection.ActivePoint); + _textView.Selection.Select(newAnchor, newActive); + } + else if (_textView.Selection.Mode != TextSelectionMode.Box) + { + //Move the caret to the start of the selection (this doesn't happen automatically if the caret was in virtual space). + //But we can't use the virtual snapshot point TranslateTo since it will remove the virtual space (because the line's line break was deleted). + _textView.Caret.MoveTo(new VirtualSnapshotPoint(_textView.Selection.Start.Position, selectionStartVirtualSpaces)); + _textView.Selection.Clear(); + } + + _textView.Caret.EnsureVisible(); + return true; + }; + + return ExecuteAction(Strings.DeleteCharToLeft, action, SelectionUpdate.ResetUnlessEmptyBox, true); + } + + private void ResetVirtualSelection() + { + //Move the caret to the same line as the active point, but at the left edge of the current selection. + VirtualSnapshotPoint start = _textView.Selection.Start; + ITextViewLine startLine = _textView.GetTextViewLineContainingBufferPosition(start.Position); + + VirtualSnapshotPoint end = _textView.Selection.End; + ITextViewLine endLine = _textView.GetTextViewLineContainingBufferPosition(end.Position); + + double leftEdge = Math.Min(startLine.GetExtendedCharacterBounds(start).Left, endLine.GetExtendedCharacterBounds(end).Left); + + ITextViewLine activeLine = (_textView.Selection.IsReversed) ? startLine : endLine; + VirtualSnapshotPoint newCaret = activeLine.GetInsertionBufferPositionFromXCoordinate(leftEdge); + + _textView.Caret.MoveTo(newCaret); + _textView.Selection.Clear(); + } + + public bool DeleteFullLine() + { + return ExecuteAction(Strings.DeleteLine, () => GetFullLines().Delete()); + } + + public bool Tabify() + { + return ConvertLeadingWhitespace(Strings.Tabify, convertTabsToSpaces: false); + } + + public bool Untabify() + { + return ConvertLeadingWhitespace(Strings.Untabify, convertTabsToSpaces: true); + } + + public bool ConvertSpacesToTabs() + { + return ExecuteAction(Strings.ConvertSpacesToTabs, delegate { return ConvertSpacesAndTabsHelper(true); }); + } + + public bool ConvertTabsToSpaces() + { + return ExecuteAction(Strings.ConvertTabsToSpaces, delegate { return ConvertSpacesAndTabsHelper(false); }); + } + + public bool NormalizeLineEndings(string replacement) + { + return ExecuteAction(Strings.NormalizeLineEndings, () => NormalizeLineEndingsHelper(replacement)); + } + + /// <summary> + /// Selects the current word. + /// </summary> + public void SelectCurrentWord() + { + TextRange currentWord = _editorPrimitives.Caret.GetCurrentWord(); + TextRange previousWord = _editorPrimitives.Caret.GetPreviousWord(); + TextRange selection = _editorPrimitives.Selection.Clone(); + + if (!_editorPrimitives.Selection.IsEmpty) + { + if (( + (selection.GetStartPoint().CurrentPosition == currentWord.GetStartPoint().CurrentPosition) && + (selection.GetEndPoint().CurrentPosition == currentWord.GetEndPoint().CurrentPosition)) || + (selection.GetStartPoint().CurrentPosition == previousWord.GetStartPoint().CurrentPosition) && + (selection.GetEndPoint().CurrentPosition == previousWord.GetEndPoint().CurrentPosition)) + { + // If the selection is already correct, don't select again, but *do* ensure + // the existing span is visible, even though we aren't moving the selection. + _textView.ViewScroller.EnsureSpanVisible(_textView.Selection.StreamSelectionSpan.SnapshotSpan, EnsureSpanVisibleOptions.MinimumScroll); + return; + } + } + + // If the current word is blank, use the previous word if it is on the same line. + if (currentWord.IsEmpty) + { + TextRange previous = currentWord.GetStartPoint().GetPreviousWord(); + if (previous.GetStartPoint().LineNumber == currentWord.GetStartPoint().LineNumber) + currentWord = previous; + } + _editorPrimitives.Selection.SelectRange(currentWord); + + } + + /// <summary> + /// Selects the enclosing. + /// </summary> + public void SelectEnclosing() + { + SnapshotSpan selectionSpan; + if (_textView.Selection.IsEmpty || _textView.Selection.Mode == TextSelectionMode.Box) + { + selectionSpan = new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0); + } + else + { + // This ignores virtual space + selectionSpan = _textView.Selection.StreamSelectionSpan.SnapshotSpan; + } + + Span span = _textStructureNavigator.GetSpanOfEnclosing(selectionSpan); + + TextRange enclosingRange = _editorPrimitives.Buffer.GetTextRange(span.Start, span.End); + _editorPrimitives.Selection.SelectRange(enclosingRange); + } + + /// <summary> + /// Selects the first child. + /// </summary> + public void SelectFirstChild() + { + SnapshotSpan selectionSpan; + if (_textView.Selection.IsEmpty || _textView.Selection.Mode == TextSelectionMode.Box) + { + selectionSpan = new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0); + } + else + { + // This ignores virtual space + selectionSpan = _textView.Selection.StreamSelectionSpan.SnapshotSpan; + } + + Span span = _textStructureNavigator.GetSpanOfFirstChild(selectionSpan); + + TextRange firstChildRange = _editorPrimitives.Buffer.GetTextRange(span.Start, span.End); + _editorPrimitives.Selection.SelectRange(firstChildRange); + } + + /// <summary> + /// Selects the next sibling. + /// </summary> + /// <param name="extendSelection">Specifies whether the selection is to be extended or a new selection is to be made.</param> + public void SelectNextSibling(bool extendSelection) + { + SnapshotSpan selectionSpan; + if (_textView.Selection.IsEmpty || _textView.Selection.Mode == TextSelectionMode.Box) + { + selectionSpan = new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0); + } + else + { + // This ignores virtual space + selectionSpan = _textView.Selection.StreamSelectionSpan.SnapshotSpan; + } + + Span span = _textStructureNavigator.GetSpanOfNextSibling(selectionSpan); + + _textView.Selection.Clear(); + if (!span.IsEmpty && extendSelection) + { + // extend the selection to the end of the next sibling + int start = (span.Start <= selectionSpan.Start) ? span.Start : selectionSpan.Start; + int end = (span.End <= selectionSpan.End) ? selectionSpan.End : span.End; + span = Span.FromBounds(start, end); + } + + TextRange nextSiblingRange = _editorPrimitives.Buffer.GetTextRange(span.Start, span.End); + _editorPrimitives.Selection.SelectRange(nextSiblingRange); + } + + /// <summary> + /// Selects the previous sibling. + /// </summary> + /// <param name="extendSelection">Specifies whether the selection is to be extended or a new selection is to be made.</param> + public void SelectPreviousSibling(bool extendSelection) + { + SnapshotSpan selectionSpan; + if (_textView.Selection.IsEmpty || _textView.Selection.Mode == TextSelectionMode.Box) + { + selectionSpan = new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0); + } + else + { + // This ignores virtual space + selectionSpan = _textView.Selection.StreamSelectionSpan.SnapshotSpan; + } + + Span span = _textStructureNavigator.GetSpanOfPreviousSibling(selectionSpan); + + _textView.Selection.Clear(); + if (!span.IsEmpty && extendSelection) + { + // extend the selection to the start of the previous sibling + int start = (span.Start <= selectionSpan.Start) ? span.Start : selectionSpan.Start; + int end = (span.End <= selectionSpan.End) ? selectionSpan.End : span.End; + span = new Span(start, end - start); + } + + TextRange previousSiblingRange = _editorPrimitives.Buffer.GetTextRange(span.Start, span.End); + _editorPrimitives.Selection.SelectRange(previousSiblingRange); + } + + /// <summary> + /// Selects all text. + /// </summary> + public void SelectAll() + { + _editorPrimitives.Selection.SelectAll(); + } + + /// <summary> + /// Extends the current selection span to the new selection end. + /// </summary> + /// <param name="newEnd"> + /// The new character position to extend the selection to. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="newEnd"/> is less than 0.</exception> + public void ExtendSelection(int newEnd) + { + _editorPrimitives.Selection.ExtendSelection(_editorPrimitives.Buffer.GetTextPoint(newEnd)); + } + + /// <summary> + /// Moves the caret to the given <paramref name="textLine"/> at the given horizontal offset <paramref name="horizontalOffset"/>. + /// </summary> + /// <param name="textLine">The <see cref="ITextViewLine"/> on which to place the caret.</param> + /// <param name="horizontalOffset">The horizontal location in the given <paramref name="textLine"/> at which to move the caret.</param> + /// <exception cref="ArgumentNullException"><paramref name="textLine"/> is null.</exception> + public void MoveCaret(ITextViewLine textLine, double horizontalOffset, bool extendSelection) + { + if (textLine == null) + { + throw new ArgumentNullException("textLine"); + } + + if (extendSelection) + { + VirtualSnapshotPoint anchor = _textView.Selection.AnchorPoint; + _textView.Caret.MoveTo(textLine, horizontalOffset); + + // It is possible that the text was modified as part of this caret move so translate the anchor + _textView.Selection.Select(anchor.TranslateTo(_textView.TextSnapshot), _textView.Caret.Position.VirtualBufferPosition); + } + else + { + // Retain the selection mode, even though we are clearing it + bool inBox = _textView.Selection.Mode == TextSelectionMode.Box; + + _textView.Selection.Clear(); + + if (inBox) + _textView.Selection.Mode = TextSelectionMode.Box; + + _textView.Caret.MoveTo(textLine, horizontalOffset); + } + } + + /// <summary> + /// Puts the caret one line up. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void MoveLineUp(bool select) + { + _editorPrimitives.Caret.MoveToPreviousLine(select); + } + + /// <summary> + /// Puts the caret one line down. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void MoveLineDown(bool select) + { + _editorPrimitives.Caret.MoveToNextLine(select); + } + + /// <summary> + /// Moves the caret one page up. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void PageUp(bool select) + { + _editorPrimitives.Caret.MovePageUp(select); + } + + /// <summary> + /// Moves the caret one page down. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void PageDown(bool select) + { + _editorPrimitives.Caret.MovePageDown(select); + } + + /// <summary> + /// Moves the caret to the end of the view line. + /// </summary> + /// <param name="select"> + /// Specifies whether selection is made as the caret is moved. + /// </param> + public void MoveToEndOfLine(bool select) + { + // If the caret is at the start of an empty line, respond by trying to position + // the caret at the smart indent location. + if (_textView.Caret.Position.BufferPosition.GetContainingLine().Extent.IsEmpty && + !_textView.Caret.InVirtualSpace) + { + if (PositionCaretWithSmartIndent(useOnlyVirtualSpace: true, extendSelection: select)) + { + _editorPrimitives.Caret.EnsureVisible(); + return; + } + } + + _editorPrimitives.Caret.MoveToEndOfViewLine(select); + } + + public void MoveToHome(bool select) + { + int newPosition = _editorPrimitives.Caret.GetFirstNonWhiteSpaceCharacterOnViewLine().CurrentPosition; + + // If the caret is already at the first non-whitespace character or + // the line is entirely whitepsace, move to the start of the view line. + if (newPosition == _editorPrimitives.Caret.CurrentPosition || + newPosition == _editorPrimitives.Caret.EndOfViewLine) + { + newPosition = _editorPrimitives.Caret.StartOfViewLine; + } + + _editorPrimitives.Caret.MoveTo(newPosition, select); + } + + public void MoveToStartOfLine(bool select) + { + _editorPrimitives.Caret.MoveToStartOfViewLine(select); + } + + /// <summary> + /// Inserts a new line at the current caret position. + /// </summary> + public bool InsertNewLine() + { + Func<bool> action = () => + { + VirtualSnapshotPoint caret = _textView.Caret.Position.VirtualBufferPosition; + ITextSnapshotLine line = caret.Position.GetContainingLine(); + ITextSnapshot snapshot = line.Snapshot; + + // todo: the following logic is duplicated in DefaultTextPointPrimitive.InsertNewLine() + // didn't call that method here because it would result in two text transactions + // ultimately everything here should probably move into primitives. + string textToInsert = TextBufferOperationHelpers.GetNewLineCharacterToInsert(line, _editorOptions); + + bool succeeded = false; + bool caretMoved = false; + EventHandler<CaretPositionChangedEventArgs> caretWatcher = delegate (object sender, CaretPositionChangedEventArgs e) + { + caretMoved = true; + }; + + // Indent unless the caret is at column 0 or the current line is empty. + // This appears to be added as a fix for Venus; which combined with our implementation of + // PositionCaretWithSmartIndent does not indent correctly on NewLine when Caret is at column 0. + bool doIndent = caret.IsInVirtualSpace || (caret.Position != _textView.Caret.ContainingTextViewLine.Start) + || (_textView.Caret.ContainingTextViewLine.Extent.Length == 0); + + try + { + using (var edit = _textView.TextBuffer.CreateEdit()) + { + _textView.Caret.PositionChanged += caretWatcher; + int searchIndexforPreviousWhitespaces = -1; + var lineContainingTrimTrailingWhitespacesSearchindex = line; // usually is the line containing caret. + + if (_textView.Selection.Mode == TextSelectionMode.Stream) + { + // This ignores virtual space + Span selection = _textView.Selection.StreamSelectionSpan.SnapshotSpan; + succeeded = edit.Replace(selection, textToInsert); + // For stream selection you should always look for trimming whitespaces previous to selection.start instead of caret position + lineContainingTrimTrailingWhitespacesSearchindex = snapshot.GetLineFromPosition(selection.Start); + searchIndexforPreviousWhitespaces = selection.Start - lineContainingTrimTrailingWhitespacesSearchindex.Start.Position; + } + else + { + var isDeleteSuccessfull = true; + searchIndexforPreviousWhitespaces = caret.Position.Position - line.Start.Position; + foreach (var span in _textView.Selection.SelectedSpans) + { + // In a box selection if the caret is forward positioned then + //we should search for whitespaces from the start of the last span since the spans are not yet deleted + if (span.End.Position == caret.Position.Position) + { + searchIndexforPreviousWhitespaces = span.Start.Position - line.Start.Position; + } + if (!edit.Delete(span)) + { + isDeleteSuccessfull = false; + } + } + if (!isDeleteSuccessfull) + return false; + succeeded = edit.Replace(new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0), + textToInsert); + } + // Trim traling whitespaces as we insert the new line as well if the editor option is set + if (_editorOptions.GetOptionValue<bool>(DefaultOptions.TrimTrailingWhiteSpaceOptionId)) + { + var previousNonWhitespaceCharacterIndex = lineContainingTrimTrailingWhitespacesSearchindex.IndexOfPreviousNonWhiteSpaceCharacter(searchIndexforPreviousWhitespaces); + + // Note: If previousNonWhiteSpaceCharacter index is -1 this will automatically default to line.start.position + var startIndexForTrailingWhitespaceSpan = lineContainingTrimTrailingWhitespacesSearchindex.Start.Position + previousNonWhitespaceCharacterIndex + 1; + var lengthOfTrailingWhitespaceSpan = searchIndexforPreviousWhitespaces - previousNonWhitespaceCharacterIndex - 1; + + if (lengthOfTrailingWhitespaceSpan != 0) // If there are any whitespaces before the caret delete them + edit.Delete(new Span(startIndexForTrailingWhitespaceSpan, lengthOfTrailingWhitespaceSpan)); + } + + // Apply all changes + succeeded = (edit.Apply() != snapshot); + } + } + finally + { + _textView.Caret.PositionChanged -= caretWatcher; + } + + if (succeeded) + { + if (doIndent) + { + caret = _textView.Caret.Position.VirtualBufferPosition; + line = caret.Position.GetContainingLine(); + + //Only attempt to auto indent if -- after the edit above -- no one moved the caret on the buffer change + //and the caret is at the start of its new line (no one did any funny edits to the buffer on the buffer change). + if ((!caretMoved) && (caret.Position == line.Start)) + { + caretMoved = PositionCaretWithSmartIndent(useOnlyVirtualSpace: false, extendSelection: false); + if (!caretMoved && caret.IsInVirtualSpace) + { + //No smart indent logic so make sure the caret is not in virtual space. + _textView.Caret.MoveTo(caret.Position); + } + } + } + ResetSelection(); + } + return succeeded; + }; + return ExecuteAction(Strings.InsertNewLine, action, SelectionUpdate.Ignore, true); + } + + + + public bool OpenLineAbove() + { + Func<bool> action = () => + { + bool result; + DisplayTextPoint textPoint = _editorPrimitives.Caret.Clone(); + if (textPoint.LineNumber == 0) + { + _editorPrimitives.Caret.MoveTo(0); + result = this.InsertNewLine(); + if (result) + { + // leave caret on the new line + _editorPrimitives.Caret.MoveTo(0); + } + } + else + { + textPoint.MoveToBeginningOfPreviousViewLine(); + textPoint.MoveToEndOfViewLine(); + _editorPrimitives.Caret.MoveTo(textPoint.CurrentPosition); + result = this.InsertNewLine(); + } + return result; + }; + return ExecuteAction(Strings.OpenLineAbove, action); + } + + public bool OpenLineBelow() + { + Func<bool> action = () => + { + _editorPrimitives.Caret.MoveToEndOfViewLine(); + return this.InsertNewLine(); + }; + return ExecuteAction(Strings.OpenLineBelow, action); + } + + /// <summary> + /// If there is a multi-line selection, indents the selection, otherwise inserts a tab at the caret location. + /// </summary> + public bool Indent() + { + bool insertTabs = _textView.Selection.Mode == TextSelectionMode.Box || !IndentOperationShouldBeMultiLine; + + Func<bool> action = () => + { + if (insertTabs) + { + return this.EditHelper(edit => + { + int tabSize = _editorOptions.GetTabSize(); + int indentSize = _editorOptions.GetIndentSize(); + bool convertTabsToSpaces = _editorOptions.IsConvertTabsToSpacesEnabled(); + bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box && + _textView.Selection.Start != _textView.Selection.End; + + // We'll need to update the start/end points if they are in virtual space, since they won't be tracking + // through a text change. + VirtualSnapshotPoint? anchorPoint = (boxSelection) ? CalculateBoxIndentForSelectionPoint(_textView.Selection.AnchorPoint, indentSize) : null; + VirtualSnapshotPoint? activePoint = (boxSelection) ? CalculateBoxIndentForSelectionPoint(_textView.Selection.ActivePoint, indentSize) : null; + + // Insert an indent for each portion of the selection (with an empty selection, there will only be a single + // span). + foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans) + { + if (!InsertIndentForSpan(span, edit, exactlyOneIndentLevel: false)) + return false; + } + + FixUpSelectionAfterBoxOperation(anchorPoint, activePoint); + + return true; + }); + } + else + { + return PerformIndentActionOnEachBufferLine(InsertSingleIndentAtPoint); + } + }; + + return ExecuteAction(Strings.InsertTab, action, (_textView.Selection.IsEmpty) ? SelectionUpdate.ClearVirtualSpace : SelectionUpdate.Ignore, ensureVisible: insertTabs); + } + + /// <summary> + /// If there is a multi-line selection, unindents the selection. If there is a single line selection, + /// removes up to a indent's worth of whitespace from before the start of the selection. If there is no selection, + /// removes up to a intent's worth of whitespace from before the caret position. + /// </summary> + public bool Unindent() + { + bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box && + _textView.Selection.Start != _textView.Selection.End; + + Func<bool> action = null; + + if (_textView.Caret.InVirtualSpace && _textView.Selection.IsEmpty) + { + this.MoveCaretToPreviousIndentStopInVirtualSpace(); + + return true; + } + else if (!boxSelection && IndentOperationShouldBeMultiLine) + { + action = () => PerformIndentActionOnEachBufferLine(RemoveIndentAtPoint); + } + else if (!boxSelection) + { + action = () => EditHelper(edit => RemoveIndentAtPoint(_textView.Selection.Start.Position, edit, failOnNonWhitespaceCharacter: false)); + } + else // Box selection + { + int columnsToRemove = DetermineMaxBoxUnindent(); + + action = () => EditHelper(edit => + { + // We'll need to update the start/end points if they are in virtual space, since they won't be tracking + // through a text change. + VirtualSnapshotPoint? anchorPoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.AnchorPoint, columnsToRemove); + VirtualSnapshotPoint? activePoint = CalculateBoxUnindentForSelectionPoint(_textView.Selection.ActivePoint, columnsToRemove); + + // Remove an indent for each portion of the selection (with an empty selection, there will only be a single + // span). + foreach (VirtualSnapshotSpan span in _textView.Selection.VirtualSelectedSpans) + { + if (!RemoveIndentAtPoint(span.Start.Position, edit, failOnNonWhitespaceCharacter: false, columnsToRemove: columnsToRemove)) + return false; + } + + FixUpSelectionAfterBoxOperation(anchorPoint, activePoint); + + return true; + + }); + } + + return ExecuteAction(Strings.RemovePreviousTab, action, SelectionUpdate.Ignore, ensureVisible: true); + } + + public bool IncreaseLineIndent() + { + Func<bool> action = () => + { + return PerformIndentActionOnEachBufferLine(InsertSingleIndentAtPoint); + }; + + return ExecuteAction(Strings.IncreaseLineIndent, action, SelectionUpdate.Ignore, ensureVisible: true); + } + + public bool DecreaseLineIndent() + { + Func<bool> action = () => + { + return PerformIndentActionOnEachBufferLine(RemoveIndentAtPoint); + }; + + return ExecuteAction(Strings.DecreaseLineIndent, action, SelectionUpdate.Ignore, ensureVisible: true); + } + + public bool DeleteBlankLines() + { + Func<bool> action = () => + { + double oldLeft = _textView.Caret.Left; + using (ITextEdit textEdit = _editorPrimitives.Buffer.AdvancedTextBuffer.CreateEdit()) + { + int startLine = _editorPrimitives.Selection.GetStartPoint().LineNumber; + int endLine = _editorPrimitives.Selection.GetEndPoint().LineNumber; + + // If the selection is empty, we follow this algorithm: + // If the current line the caret is on is blank or the caret is at the end of the line + // delete all blank lines that occur later in the file. + // If the caret is not on a blank line and there are no blank lines below the caret + // then delete all blank lines between the caret line and the next non-blank line. + // This matches VS9 behavior. + if (_editorPrimitives.Selection.IsEmpty) + { + // First search downwards to see if there are blank lines to delete if we are at the end of the + // line or if we are on a blank line. + TextPoint startOfLine = _editorPrimitives.Buffer.GetTextPoint(startLine, 0); + + if (IsPointOnBlankLine(startOfLine) || + (_editorPrimitives.Caret.CurrentPosition == _editorPrimitives.Caret.EndOfLine)) //Caret is at the physical end of a line + { + while (endLine < _editorPrimitives.Selection.AdvancedSelection.TextView.TextSnapshot.LineCount - 1) + { + TextPoint startOfNextLine = _editorPrimitives.Buffer.GetTextPoint(endLine + 1, 0); + if (!IsPointOnBlankLine(startOfNextLine)) + { + break; + } + endLine++; + } + } + + // If there are no blank lines below the current line and + // this is not a blank line, look up to see if there are any + // blank lines directly above the current one + if (startLine == endLine) + { + if (!IsPointOnBlankLine(startOfLine)) + { + while (startLine > 0) + { + startOfLine = _editorPrimitives.Buffer.GetTextPoint(startLine - 1, 0); + if (!IsPointOnBlankLine(startOfLine)) + { + break; + } + startLine--; + } + } + } + } + + for (int i = startLine; i <= endLine; i++) + { + TextPoint startPoint = _editorPrimitives.Buffer.GetTextPoint(i, 0); + + if (IsPointOnBlankLine(startPoint)) + { + TextPoint startOfNextLine = startPoint.Clone(); + startOfNextLine.MoveToBeginningOfNextLine(); + if (!textEdit.Delete(Span.FromBounds(startPoint.CurrentPosition, startOfNextLine.CurrentPosition))) + return false; + } + } + textEdit.Apply(); + + if (textEdit.Canceled) + return false; + + _textView.Caret.EnsureVisible(); + ITextViewLine newLine = _textView.Caret.ContainingTextViewLine; + _textView.Caret.MoveTo(newLine, oldLeft); + + return true; + } + }; + + return ExecuteAction(Strings.DeleteBlankLines, action); + } + + public bool DeleteHorizontalWhiteSpace() + { + return ExecuteAction(Strings.DeleteHorizontalWhiteSpace, DeleteHorizontalWhitespace); + } + + public Boolean InsertFinalNewLine() + { + Func<bool> action = () => + { + var buffer = _textView.TextBuffer; + return TextBufferOperationHelpers.TryInsertFinalNewLine(buffer, _editorOptions); + }; + + return ExecuteAction(Strings.InsertFinalNewLine, action, SelectionUpdate.Ignore, true); + } + + public bool TrimTrailingWhiteSpace() + { + Func<bool> action = () => + { + ITextSnapshot snapshot = _textView.TextSnapshot; + var startLine = snapshot.GetLineFromPosition(_textView.Selection.Start.Position); + var endLine = snapshot.GetLineFromPosition(_textView.Selection.End.Position); + + return TrimTrailingWhiteSpace(startLine, endLine); + }; + + return ExecuteAction(Strings.TrimTrailingWhitespace, action, SelectionUpdate.Ignore, true); + } + + private bool TrimTrailingWhiteSpace(ITextSnapshotLine line) + { + return TrimTrailingWhiteSpace(line, line); + } + + private bool TrimTrailingWhiteSpace(ITextSnapshotLine startLine, ITextSnapshotLine endLine) + { + Debug.Assert(startLine.LineNumber <= endLine.LineNumber); + + IWpfTextView view = _textView as IWpfTextView; + bool isEditMade = false; + bool success = true; + + using (ITextEdit edit = view.TextBuffer.CreateEdit()) + { + var currentSnapshot = _textView.TextBuffer.CurrentSnapshot; + for (int i = startLine.LineNumber; i <= endLine.LineNumber; i++) + { + var currentTrailingSpan = GetTrailingWhitespaceSpanToDelete(currentSnapshot.GetLineFromLineNumber(i)); + if (currentTrailingSpan != null && currentTrailingSpan.HasValue && !currentTrailingSpan.Value.IsEmpty) + { + isEditMade = true; + edit.Delete(currentTrailingSpan.Value); + } + } + + if (isEditMade) + { + success = (edit.Apply() != currentSnapshot); + } + } + + return success; + } + + private Span? GetTrailingWhitespaceSpanToDelete(ITextSnapshotLine line) + { + int indexOfLastNonWhitespaceCharacter = -1; + + for (int i = line.End.Position - 1; i >= line.Start.Position; i--) + { + if (!char.IsWhiteSpace(line.Snapshot[i])) + { + break; + } + indexOfLastNonWhitespaceCharacter = i; + } + + if (indexOfLastNonWhitespaceCharacter != -1) + return new Span(indexOfLastNonWhitespaceCharacter, line.End.Position - indexOfLastNonWhitespaceCharacter); + return null; + } + + /// <summary> + /// Inserts the given text at the current caret position. + /// </summary> + /// <param name="text"> + /// The text to be inserted in the buffer. + /// </param> + /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception> + public bool InsertText(string text) + { + return this.InsertText(text, true); + } + + public bool InsertProvisionalText(string text) + { + return this.InsertText(text, false); + } + + public ITrackingSpan ProvisionalCompositionSpan { get { return _immProvisionalComposition; } } + + public IEditorOptions Options { get { return _editorOptions; } } + + public string SelectedText + { + get + { + string text; + if (_textView.Selection.SelectedSpans.Count > 1) + { + text = string.Join(_editorOptions.GetNewLineCharacter(), _textView.Selection.SelectedSpans + .Select((span) => span.GetText()) + .ToArray()); + + // Append one last newline character + text += _editorOptions.GetNewLineCharacter(); + } + else + { + text = _textView.Selection.StreamSelectionSpan.GetText(); + } + + return text; + } + } + + /// <summary> + /// Selects the given line. + /// </summary> + /// <param name="extendSelection"> + /// Specifies whether the selection is to be extended or a new selection is to be made. + /// </param> + public void SelectLine(ITextViewLine viewLine, bool extendSelection) + { + if (viewLine == null) + throw new ArgumentNullException("viewLine"); + + SnapshotPoint anchor; + SnapshotPoint active; + if (!extendSelection || _textView.Selection.IsEmpty) + { + // This is always going to be a forward span + anchor = viewLine.Start; + active = viewLine.EndIncludingLineBreak; + } + else + { + ITextViewLine anchorLine = _textView.GetTextViewLineContainingBufferPosition(_textView.Selection.AnchorPoint.Position); + if (_textView.Selection.IsReversed && (!_textView.Selection.AnchorPoint.IsInVirtualSpace) && + (_textView.Selection.AnchorPoint.Position == anchorLine.Start) && + anchorLine.Start.Position > 0) + { + //In a reversed selection, an anchor that starts at the begining of a line really corresponds + //to the end of the previous line. + anchorLine = _textView.GetTextViewLineContainingBufferPosition(anchorLine.Start - 1); + } + + if (viewLine.Start < anchorLine.Start) + { + //Grow the selection (creating a reversed selection) + anchor = anchorLine.EndIncludingLineBreak; + active = viewLine.Start; + } + else + { + anchor = anchorLine.Start; + active = viewLine.EndIncludingLineBreak; + } + } + + //Only try and show the caret, not the entire selection. + this.SelectAndMoveCaret(new VirtualSnapshotPoint(anchor), new VirtualSnapshotPoint(active), TextSelectionMode.Stream, null); + _textView.Caret.EnsureVisible(); + } + + /// <summary> + /// Resets any selection in the text. + /// </summary> + public void ResetSelection() + { + _textView.Selection.Clear(); + } + + /// <summary> + /// Deletes the selection, if present, or the next character in the text buffer. + /// </summary> + public bool Delete() + { + bool emptyBox = IsEmptyBoxSelection(); + NormalizedSnapshotSpanCollection boxDeletions = null; + + // First, handle cases that don't require edits + if (_textView.Selection.IsEmpty) + { + if (_textView.Caret.Position.BufferPosition.Position == _textView.TextSnapshot.Length) + { + return true; + } + } + // If the entire selection is empty and in virtual space, clear it + else if (_textView.Selection.VirtualSelectedSpans.All(s => s.SnapshotSpan.IsEmpty && s.IsInVirtualSpace)) + { + this.ResetVirtualSelection(); + return true; + } + else if (emptyBox) // empty box selection, make sure it is valid + { + List<SnapshotSpan> spans = new List<SnapshotSpan>(); + + foreach (var span in _textView.Selection.SelectedSpans) + { + var line = span.Start.GetContainingLine(); + if (span.Start < line.End) + { + spans.Add(_textView.GetTextElementSpan(span.Start)); + } + } + + // If there is nothing to delete, clear the selection + if (spans.Count == 0) + { + _textView.Caret.MoveTo(_textView.Selection.Start); + _textView.Selection.Clear(); + return true; + } + + boxDeletions = new NormalizedSnapshotSpanCollection(spans); + } + + + // Now handle cases that require edits + Func<bool> action = () => + { + if (_textView.Selection.IsEmpty) + { + CaretPosition position = _textView.Caret.Position; + if (position.VirtualBufferPosition.IsInVirtualSpace) + { + string whitespace = GetWhitespaceForVirtualSpace(position.VirtualBufferPosition); + SnapshotSpan span = _textView.GetTextElementSpan(_textView.Caret.Position.VirtualBufferPosition.Position); + + return ReplaceHelper(span, whitespace); + } + else + { + return DeleteHelper(_textView.GetTextElementSpan(position.VirtualBufferPosition.Position)); + } + } + else + { + // The selection is non-empty, so delete selected spans + NormalizedSnapshotSpanCollection deletion = _textView.Selection.SelectedSpans; + + // Unless it is an empty box selection, so treat it as a delete on each line + if (emptyBox && boxDeletions != null) + deletion = boxDeletions; + + int selectionStartVirtualSpaces = _textView.Selection.Start.VirtualSpaces; + bool succeeded = DeleteHelper(deletion); + + if (succeeded && (_textView.Selection.Mode != TextSelectionMode.Box)) + { + //Move the caret to the start of the selection (this doesn't happen automatically if the caret was in virtual space). + //But we can't use the virtual snapshot point TranslateTo since it will remove the virtual space (because the line's line break was deleted). + _textView.Caret.MoveTo(new VirtualSnapshotPoint(_textView.Selection.Start.Position, selectionStartVirtualSpaces)); + _textView.Selection.Clear(); + } + + return succeeded; + } + }; + + return ExecuteAction(Strings.DeleteText, action, SelectionUpdate.ResetUnlessEmptyBox, true); + } + + /// <summary> + /// Replace text selection with the new text. + /// </summary> + /// <param name="text"> + /// The new text that will replace the old selection. + /// </param> + /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception> + public bool ReplaceSelection(string text) + { + // Validate + if (text == null) + { + throw new ArgumentNullException("text"); + } + + Func<bool> action = () => + { + return ReplaceHelper(_textView.Selection.VirtualSelectedSpans, text); + }; + + return ExecuteAction(Strings.ReplaceSelectionWith + text, action); + } + + /// <summary> + /// Replace text from the given span with the new text. + /// </summary> + /// <param name="span"> + /// The span of text to replace. + /// </param> + /// <param name="text"> + /// The new text that will replace the old selection. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/> is greater than the length in the TextBuffer.</exception> + public bool ReplaceText(Span span, string text) + { + // Validate + if (span.End > _textView.TextSnapshot.Length) + { + throw new ArgumentOutOfRangeException("span"); + } + + Func<bool> action = () => + { + return ReplaceHelper(span, text); + }; + + return ExecuteAction(Strings.ReplaceText, action, SelectionUpdate.Reset, true); + } + + /// <summary> + /// Replaces all matching occurrences of the given string. + /// </summary> + /// <param name="searchText"> + /// Text to match. + /// </param> + /// <param name="replaceText"> + /// Text used in replace. + /// </param> + /// <param name="matchCase"> + /// True if search should match case, false otherwise. + /// </param> + /// <param name="matchWholeWord"> + /// True if search should match whole word, false otherwise. + /// </param> + /// <param name="useRegularExpressions"> + /// True if search should use regular expression, false otherwise. + /// </param> + /// <exception cref="ArgumentNullException"><paramref name="searchText"/> is null.</exception> + /// <remarks>If one of the matches found is read only, none of the matches will be replaced.</remarks> + public int ReplaceAllMatches(string searchText, string replaceText, bool matchCase, bool matchWholeWord, bool useRegularExpressions) + { + if (searchText == null) + { + throw new ArgumentNullException("searchText"); + } + + FindData findData = new FindData(searchText, _textView.TextSnapshot); + findData.TextStructureNavigator = _textStructureNavigator; + if (matchCase) + { + findData.FindOptions = findData.FindOptions | FindOptions.MatchCase; + } + if (matchWholeWord) + { + findData.FindOptions = findData.FindOptions | FindOptions.WholeWord; + } + if (useRegularExpressions) + { + findData.FindOptions = findData.FindOptions | FindOptions.UseRegularExpressions; + } + + int numberOfReplacementsMade = 0; + + // Now, use FindLogic to find all matching occurrences + Collection<SnapshotSpan> textSpanMatches = _factory.TextSearchService.FindAll(findData); + + if (textSpanMatches != null && textSpanMatches.Count > 0) + { + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.ReplaceAll)) + { + AddBeforeTextBufferChangePrimitive(); + + bool replaceFailed = false; + using (ITextEdit textEdit = _textView.TextBuffer.CreateEdit()) + { + // If we are not using regular expressions, simply replace all matches with replaceText + if (!useRegularExpressions) + { + // Perform each replace, and create an undo unit to track its changes + foreach (SnapshotSpan snapSpan in textSpanMatches) + { + if (!textEdit.Replace(snapSpan, replaceText)) + { + replaceFailed = true; + break; + } + } + } + else + { + // Since we are using regular expressions, each replace text may be dependent on the matching text. + // Therefore, bring up a regular expression engine, and for each match, we will take that text and use Regex.Replace() on the string. + Regex regex = new Regex(searchText, (!matchCase ? RegexOptions.IgnoreCase : 0)); + + foreach (SnapshotSpan textSpan in textSpanMatches) + { + string newText = regex.Replace(textSpan.GetText(), replaceText); + if (!textEdit.Replace(textSpan, newText)) + { + replaceFailed = true; + break; + } + } + } + + AddAfterTextBufferChangePrimitive(); + + if (!replaceFailed) + { + textEdit.Apply(); + + if (!textEdit.Canceled) + { + numberOfReplacementsMade = textSpanMatches.Count; + undoTransaction.Complete(); + } + } + } + } + } + + return numberOfReplacementsMade; + } + + /// <summary> + /// Copies the selected text to clip board. + /// </summary> + /// <exception cref="InsufficientMemoryException"> is thrown if there is not sufficient memory to complete the operation.</exception> + public bool CopySelection() + { + if (!_textView.Selection.IsEmpty) + { + return PrepareClipboardSelectionCopy().Invoke(); + } + else if (!IsPointOnBlankViewLine(_editorPrimitives.Caret) || + (_editorOptions.GetOptionValue<bool>(DefaultTextViewOptions.CutOrCopyBlankLineIfNoSelectionId))) + { + return PrepareClipboardFullLineCopy(GetFullLines()).Invoke(); + } + else + { + return true; + } + } + + public bool CutFullLine() + { + DisplayTextRange fullSelectedLines = GetFullLines(); + + if (!fullSelectedLines.TextBuffer.AdvancedTextBuffer.IsReadOnly(fullSelectedLines.AdvancedTextRange)) + { + Func<bool> putCopiedLineToClipboard = PrepareClipboardFullLineCopy(fullSelectedLines); + + return ExecuteAction(Strings.CutLine, () => fullSelectedLines.Delete() && putCopiedLineToClipboard.Invoke()); + } + else + { + return false; + } + } + + /// <summary> + /// Cuts the selected text. + /// </summary> + /// <exception cref="InsufficientMemoryException"> is thrown if there is not sufficient memory to complete the operation.</exception> + public bool CutSelection() + { + // no-op if can't do any cut operation + if (_textView.Selection.IsEmpty && !CanCut) + return true; + + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.CutSelection)) + { + this.AddBeforeTextBufferChangePrimitive(); + + bool performedCut = false; + + if (!_textView.Selection.IsEmpty) + { + Func<bool> putCopiedSelectionToClipboard = PrepareClipboardSelectionCopy(); + + performedCut = this.Delete() && putCopiedSelectionToClipboard.Invoke(); + } + else + { + DisplayTextRange lineRange = GetFullLines(); + double caretX = _textView.Caret.Left; + Func<bool> putCopiedLineToClipboard = PrepareClipboardFullLineCopy(lineRange); + + if (lineRange.Delete() && putCopiedLineToClipboard.Invoke()) + { + performedCut = true; + + // Move the caret back to the starting x coordinate on it's current line + _textView.Caret.MoveTo(_textView.Caret.ContainingTextViewLine, caretX); + } + } + + if (performedCut) + { + _textView.Caret.EnsureVisible(); + this.AddAfterTextBufferChangePrimitive(); + undoTransaction.Complete(); + } + else + { + undoTransaction.Cancel(); + } + + return performedCut; + } + } + + /// <summary> + /// Pastes text from the clipboard to the text buffer. + /// </summary> + public bool Paste() + { + string text = null; + bool dataHasLineCutCopyTag = false; + bool dataHasBoxCutCopyTag = false; + + // Clipboard may throw exceptions, so enclose Clipboard calls in a try-catch block + try + { + IDataObject dataObj = Clipboard.GetDataObject(); + + if (dataObj == null || !dataObj.GetDataPresent(typeof(string))) + { + return true; + } + + text = (string)dataObj.GetData(DataFormats.UnicodeText); + if (text == null) + { + text = (string)dataObj.GetData(DataFormats.Text); + } + + dataHasLineCutCopyTag = dataObj.GetDataPresent(_clipboardLineBasedCutCopyTag); + dataHasBoxCutCopyTag = dataObj.GetDataPresent(_boxSelectionCutCopyTag); + } + catch (System.Runtime.InteropServices.ExternalException) + { + // TODO: Log error + return false; + } + catch (OutOfMemoryException) + { + // silently fail on out of memory exceptions. + // the clipboard also throws out of memory exceptions when the data in the clipboard is corrupt + // see bug 780687 + return false; + } + + if (text != null) + { + if (dataHasLineCutCopyTag && _textView.Selection.IsEmpty) + { + //this only applies if the data was copied from the editor itself in the special cut/copy + //mode when the caret is placed on a line and it's copied/cut without any selection + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.Paste)) + { + this.AddBeforeTextBufferChangePrimitive(); + + SnapshotPoint insertionPoint = _textView.Caret.Position.BufferPosition.GetContainingLine().Start; + + using (ITextEdit edit = _textView.TextBuffer.CreateEdit()) + { + if (!edit.Insert(insertionPoint.Position, text)) + return false; + + edit.Apply(); + } + + this.AddAfterTextBufferChangePrimitive(); + + undoTransaction.Complete(); + + _textView.Caret.EnsureVisible(); + + return true; + } + } + else if (dataHasBoxCutCopyTag) + { + // If the caret is on a blank line, treat this almost like a normal stream + // insertion, but with extra whitespace on the beginning of each line to + // correctly maintain column position. + if (_textView.Selection.IsEmpty && IsPointOnBlankViewLine(_editorPrimitives.Caret)) + { + // We want each line to be at the column position of the caret, which is + // the editor primitives caret column + virtual spaces. + string whitespace = GetWhitespaceForDisplayColumn(_editorPrimitives.Caret.Column + _textView.Caret.Position.VirtualBufferPosition.VirtualSpaces); + + // Collect the lines from the clipboard + List<string> lines = new List<string>(); + using (StringReader reader = new StringReader(text)) + { + for (string lineText = reader.ReadLine(); lineText != null; lineText = reader.ReadLine()) + { + lines.Add(lineText); + } + } + + string streamText = string.Join(_editorOptions.GetNewLineCharacter() + whitespace, lines); + return this.InsertText(streamText.ToString(), true, Strings.Paste, isOverwriteModeEnabled: false); + } + else + { + VirtualSnapshotPoint unusedStart, unusedEnd; + + return this.InsertTextAsBox(text, out unusedStart, out unusedEnd, Strings.Paste); + } + } + else + { + return this.InsertText(text, true, Strings.Paste, isOverwriteModeEnabled: false); + } + } + else + { + return true; + } + } + + /// <summary> + /// Gets whether or not a paste operation can happen. + /// </summary> + public bool CanPaste + { + get + { + // Clipboard may throw exceptions, so enclose Clipboard calls in a try-catch block + try + { + return Clipboard.ContainsText() && !_textView.TextSnapshot.TextBuffer.IsReadOnly(_editorPrimitives.Caret.CurrentPosition); + } + catch (System.Runtime.InteropServices.ExternalException) + { + // TODO: Log error + return false; + } + } + } + + public bool CanDelete + { + get + { + if (_editorPrimitives.Selection.IsEmpty) + { + if (_editorPrimitives.Caret.CurrentPosition < _textView.TextSnapshot.Length) + { + return !_textView.TextSnapshot.TextBuffer.IsReadOnly(_editorPrimitives.Caret.CurrentPosition); + } + + return false; + } + else + { + return !_editorPrimitives.Selection.AdvancedTextRange.Snapshot.TextBuffer.IsReadOnly(_editorPrimitives.Selection.AdvancedTextRange); + } + } + } + + public bool CanCut + { + get + { + if (_editorPrimitives.Selection.IsEmpty) + { + if (!IsPointOnBlankViewLine(_editorPrimitives.Caret) || + _editorOptions.GetOptionValue<bool>(DefaultTextViewOptions.CutOrCopyBlankLineIfNoSelectionId)) + { + ITextViewLine caretLine = _editorPrimitives.Caret.AdvancedCaret.ContainingTextViewLine; + return !caretLine.Snapshot.TextBuffer.IsReadOnly(caretLine.ExtentIncludingLineBreak); + } + } + else + { + return !_editorPrimitives.Selection.AdvancedTextRange.Snapshot.TextBuffer.IsReadOnly(_editorPrimitives.Selection.AdvancedTextRange); + } + + return false; + } + } + + /// <summary> + /// Sets the caret at the start of the specified line. + /// </summary> + /// <param name="lineNumber"> + /// The line number to set the caret at. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/>is less than 0 or greater than the line number of the last line in the TextBuffer.</exception> + public void GotoLine(int lineNumber) + { + // Validate + if (lineNumber < 0 || lineNumber > _textView.TextSnapshot.LineCount - 1) + throw new ArgumentOutOfRangeException("lineNumber"); + + ITextSnapshotLine line = _textView.TextSnapshot.GetLineFromLineNumber(lineNumber); + + _textView.Caret.MoveTo(line.Start); + _textView.Selection.Clear(); + _textView.ViewScroller.EnsureSpanVisible(new SnapshotSpan(_textView.Caret.Position.BufferPosition, 0)); + } + + /// <summary> + /// The text view on which these operations work. + /// </summary> + public ITextView TextView + { + get + { + return _textView; + } + } + + /// <summary> + /// Scrolls the view either up by one line, + /// reposition caret only if it is scrolled off the page. + /// It will reposition the cursor to the newly scrolled line + /// at the bottom of the view. + /// </summary> + public void ScrollUpAndMoveCaretIfNecessary() + { + this.ScrollByLineAndMoveCaretIfNecessary(ScrollDirection.Up); + } + + public void SwapCaretAndAnchor() + { + _textView.Selection.Select(_textView.Selection.ActivePoint, + _textView.Selection.AnchorPoint); + _textView.Caret.MoveTo(_textView.Selection.ActivePoint); + _textView.Caret.EnsureVisible(); + } + + /// <summary> + /// Scrolls the view either up by one line, + /// reposition caret only if it is scrolled off the page. + /// It will reposition the cursor to the newly scrolled line + /// at the top of the view. + /// </summary> + public void ScrollDownAndMoveCaretIfNecessary() + { + this.ScrollByLineAndMoveCaretIfNecessary(ScrollDirection.Down); + } + + /// <summary> + /// Transposes character at the cursor with next character. + /// </summary> + public bool TransposeCharacter() + { + return ExecuteAction(Strings.TransposeCharacter, () => _editorPrimitives.Caret.TransposeCharacter()); + } + + /// <summary> + /// Transposes line containing the cursor with the next line. + /// </summary> + public bool TransposeLine() + { + // Only transpose lines if there are more than 2 lines + if (_textView.TextSnapshot.LineCount < 2) + { + return true; + } + + return ExecuteAction(Strings.TransposeLine, () => _editorPrimitives.Caret.TransposeLine()); + } + + public bool TransposeWord() + { + TextRange currentWord = _editorPrimitives.Caret.GetCurrentWord(); + if (currentWord.IsEmpty) + { + currentWord = currentWord.GetStartPoint().GetPreviousWord(); + } + + if (currentWord.GetEndPoint().CurrentPosition == _editorPrimitives.Buffer.GetEndPoint().CurrentPosition) + { + return true; + } + + Func<bool> action = () => + { + TextRange nextWord = currentWord.GetEndPoint().GetNextWord(); + + Func<TextRange, bool> keepSearching = (tr) => + { + if (tr.GetEndPoint().CurrentPosition == _editorPrimitives.Buffer.GetEndPoint().CurrentPosition) + { + return false; + } + if (!tr.IsEmpty && char.IsLetterOrDigit(tr.GetText()[0])) + { + return false; + } + return true; + }; + + while (keepSearching(nextWord)) + { + nextWord = nextWord.GetEndPoint().GetNextWord(); + } + + int newCaretPosition = nextWord.GetEndPoint().CurrentPosition; + + using (ITextEdit textEdit = _editorPrimitives.Buffer.AdvancedTextBuffer.CreateEdit()) + { + if (!textEdit.Replace(currentWord.AdvancedTextRange.Span, nextWord.GetText())) + return false; + if (!textEdit.Replace(nextWord.AdvancedTextRange.Span, currentWord.GetText())) + return false; + + textEdit.Apply(); + + if (textEdit.Canceled) + return false; + } + + _editorPrimitives.Caret.MoveTo(newCaretPosition); + + return true; + }; + + return ExecuteAction(Strings.TransposeWord, action); + } + + /// <summary> + /// Converts letters to the specified case in select text or next character to the cursor if select is empty. + /// </summary> + /// <param name="letterCase"> + /// The letter case to convert to. + /// </param> + private bool ChangeCase(LetterCase letterCase) + { + NormalizedSnapshotSpanCollection spans; + SelectionUpdate selectionUpdate; + if (_textView.Selection.IsEmpty) + { + // Do nothing if caret is at the end of buffer and there is no selection + if (_textView.Caret.Position.BufferPosition == _textView.TextSnapshot.Length) + return true; + + // If the caret is at the physical end of a line, just move to the next line. + VirtualSnapshotPoint caret = _textView.Caret.Position.VirtualBufferPosition; + ITextSnapshotLine line = _textView.TextSnapshot.GetLineFromPosition(caret.Position); + + if (line.End == caret.Position) + { + _textView.Caret.MoveTo(line.EndIncludingLineBreak); + return true; + } + + spans = new NormalizedSnapshotSpanCollection(_textView.GetTextElementSpan(caret.Position)); + selectionUpdate = SelectionUpdate.Ignore; + } + else + { + spans = _textView.Selection.SelectedSpans; + selectionUpdate = SelectionUpdate.Preserve; + } + + Func<bool> action = () => + { + return EditHelper((edit) => + { + foreach (var span in spans) + { + string textToConvert = span.GetText(); + string convertedCaseText = (letterCase == LetterCase.Uppercase) + ? textToConvert.ToUpper(System.Globalization.CultureInfo.CurrentCulture) + : textToConvert.ToLower(System.Globalization.CultureInfo.CurrentCulture); + + if (!edit.Replace(span, convertedCaseText)) + return false; + } + + return true; + }); + + }; + return this.ExecuteAction((letterCase == LetterCase.Uppercase ? Strings.MakeUppercase : Strings.MakeLowercase), action, selectionUpdate, true); + } + + /// <summary> + /// Converts lowercase letters to uppercase in select text or next character to the cursor if select is empty. + /// </summary> + public bool MakeUppercase() + { + return ChangeCase(LetterCase.Uppercase); + } + + /// <summary> + /// Converts uppercase letters to lowercase in select text or next character to the cursor if select is empty. + /// </summary> + public bool MakeLowercase() + { + return ChangeCase(LetterCase.Lowercase); + } + + public bool Capitalize() + { + return ExecuteAction(Strings.Capitalize, () => _editorPrimitives.Selection.Capitalize()); + } + + public bool ToggleCase() + { + return ExecuteAction(Strings.ToggleCase, () => _editorPrimitives.Selection.ToggleCase()); + } + + public bool InsertFile(string filePath) + { + Func<bool> action = + () => + { + IContentType contentTypeInert = _factory.ContentTypeRegistryService.GetContentType("inert"); + bool unused; + ITextDocument document = _factory.TextDocumentFactoryService.CreateAndLoadTextDocument(filePath, contentTypeInert, attemptUtf8Detection: true, characterSubstitutionsOccurred: out unused); + ITrackingPoint oldCaretLocation = _textView.TextSnapshot.CreateTrackingPoint(_editorPrimitives.Caret.CurrentPosition, PointTrackingMode.Negative); + if (!_editorPrimitives.Caret.InsertText(document.TextBuffer.CurrentSnapshot.GetText())) + return false; + _editorPrimitives.Caret.MoveTo(oldCaretLocation.GetPosition(_textView.TextSnapshot)); + return true; + }; + + return ExecuteAction(Strings.InsertFile, action); + } + + public void ScrollPageUp() + { + _editorPrimitives.View.ScrollPageUp(); + } + + public void ScrollPageDown() + { + _editorPrimitives.View.ScrollPageDown(); + } + + public void ScrollColumnLeft() + { + IWpfTextView wpfTextView = _textView as IWpfTextView; + if (wpfTextView != null) + { + // A column is defined as the width of a space in the default font + _textView.ViewScroller.ScrollViewportHorizontallyByPixels(wpfTextView.FormattedLineSource.ColumnWidth * -1.0); + } + } + + public void ScrollColumnRight() + { + IWpfTextView wpfTextView = _textView as IWpfTextView; + if (wpfTextView != null) + { + // A column is defined as the width of a space in the default font + _textView.ViewScroller.ScrollViewportHorizontallyByPixels(wpfTextView.FormattedLineSource.ColumnWidth); + } + } + + public void ScrollLineBottom() + { + _editorPrimitives.View.MoveLineToBottom(_editorPrimitives.Caret.LineNumber); + } + + public void ScrollLineTop() + { + _editorPrimitives.View.MoveLineToTop(_editorPrimitives.Caret.LineNumber); + } + + public void ScrollLineCenter() + { + _editorPrimitives.View.Show(_editorPrimitives.Caret, HowToShow.Centered); + } + + public void AddAfterTextBufferChangePrimitive() + { + // Create an AfterTextBufferChangeUndoPrimitive undo primitive using the current caret position and selection. + AfterTextBufferChangeUndoPrimitive afterTextBufferChangeUndoPrimitive = + AfterTextBufferChangeUndoPrimitive.Create(_textView, _undoHistory); + _undoHistory.CurrentTransaction.AddUndo(afterTextBufferChangeUndoPrimitive); + } + + public void AddBeforeTextBufferChangePrimitive() + { + // Create a BeforeTextBufferChangeUndoPrimitive undo primitive using the current caret position and selection. + BeforeTextBufferChangeUndoPrimitive beforeTextBufferChangeUndoPrimitive = + BeforeTextBufferChangeUndoPrimitive.Create(_textView, _undoHistory); + _undoHistory.CurrentTransaction.AddUndo(beforeTextBufferChangeUndoPrimitive); + } + + public void ZoomIn() + { + IWpfTextView wpfTextView = _textView as IWpfTextView; + if (wpfTextView != null && wpfTextView.Roles.Contains(PredefinedTextViewRoles.Zoomable)) + { + double zoomLevel = wpfTextView.ZoomLevel * ZoomConstants.ScalingFactor; + if (zoomLevel < ZoomConstants.MaxZoom || Math.Abs(zoomLevel - ZoomConstants.MaxZoom) < 0.00001) + { + wpfTextView.Options.GlobalOptions.SetOptionValue(DefaultWpfViewOptions.ZoomLevelId, zoomLevel); + } + } + } + + public void ZoomOut() + { + IWpfTextView wpfTextView = _textView as IWpfTextView; + if (wpfTextView != null && wpfTextView.Roles.Contains(PredefinedTextViewRoles.Zoomable)) + { + double zoomLevel = wpfTextView.ZoomLevel / ZoomConstants.ScalingFactor; + if (zoomLevel > ZoomConstants.MinZoom || Math.Abs(zoomLevel - ZoomConstants.MinZoom) < 0.00001) + { + wpfTextView.Options.GlobalOptions.SetOptionValue(DefaultWpfViewOptions.ZoomLevelId, zoomLevel); + } + } + } + + public void ZoomTo(double zoomLevel) + { + IWpfTextView wpfTextView = _textView as IWpfTextView; + if (wpfTextView != null && wpfTextView.Roles.Contains(PredefinedTextViewRoles.Zoomable)) + { + wpfTextView.Options.GlobalOptions.SetOptionValue(DefaultWpfViewOptions.ZoomLevelId, zoomLevel); + } + } + + #endregion // IEditorOperations Members + + #region Virtual Space to Whitespace helpers + + public string GetWhitespaceForVirtualSpace(VirtualSnapshotPoint point) + { + if (!point.IsInVirtualSpace) + return string.Empty; + + return GetWhiteSpaceForPositionAndVirtualSpace(point.Position, point.VirtualSpaces); + } + + internal string GetWhiteSpaceForPositionAndVirtualSpace(SnapshotPoint position, int virtualSpaces) + { + return GetWhiteSpaceForPositionAndVirtualSpace(position, virtualSpaces, useBufferPrimitives: false); + } + + internal string GetWhiteSpaceForPositionAndVirtualSpace(SnapshotPoint position, int virtualSpaces, bool useBufferPrimitives) + { + return GetWhiteSpaceForPositionAndVirtualSpace(position, virtualSpaces, useBufferPrimitives, convertTabsToSpaces: _textView.Options.IsConvertTabsToSpacesEnabled()); + } + + /// <summary> + /// Given a buffer point and a count of virtual spaces, determine the correct whitespace, as a sequence of tabs/spaces, to get + /// from the buffer point to the virtual point. Note that the buffer position doesn't have to be at the end of the line, so this + /// method can be used to calculate whitespace offset in arbitrary contexts. + /// </summary> + /// <param name="position">The buffer position to start from.</param> + /// <param name="virtualSpaces">The count of virtual spaces to generate whitespace for.</param> + /// <param name="useBufferPrimitives">Whether to use buffer or view primitives. Buffer primitives must be used if the given + /// <paramref name="position"/> could be hidden in the view.</param> + /// <param name="convertTabsToSpaces">Whether tabs or spaces should be use.</param> + /// <returns>A non-null string of whitespace.</returns> + internal string GetWhiteSpaceForPositionAndVirtualSpace(SnapshotPoint position, int virtualSpaces, bool useBufferPrimitives, bool convertTabsToSpaces) + { + string textToInsert; + if (!convertTabsToSpaces) + { + int tabSize = _textView.Options.GetTabSize(); + + int columnOfEndOfLine; + if (useBufferPrimitives) + columnOfEndOfLine = _editorPrimitives.Buffer.GetTextPoint(position).Column; + else + columnOfEndOfLine = _editorPrimitives.View.GetTextPoint(position).DisplayColumn; + + int caretColumn = columnOfEndOfLine + virtualSpaces; + + int spacesAfterPreviousTabStop = caretColumn % tabSize; + int columnOfPreviousTabStop = caretColumn - spacesAfterPreviousTabStop; + + int requiredTabs = ((columnOfPreviousTabStop - columnOfEndOfLine) + tabSize - 1) / tabSize; + + if (requiredTabs > 0) + textToInsert = new string('\t', requiredTabs) + new string(' ', spacesAfterPreviousTabStop); + else + textToInsert = new string(' ', virtualSpaces); + } + else + { + textToInsert = new string(' ', virtualSpaces); + } + + return textToInsert; + } + + internal string GetWhitespaceForDisplayColumn(int caretColumn) + { + string textToInsert; + if (!_textView.Options.IsConvertTabsToSpacesEnabled()) + { + int tabSize = _textView.Options.GetTabSize(); + + int spacesToInsert = caretColumn % tabSize; + textToInsert = new string(' ', spacesToInsert); + + int columnOfPreviousTabStop = caretColumn - spacesToInsert; + + int requiredTabs = ((columnOfPreviousTabStop) + tabSize - 1) / tabSize; + + if (requiredTabs > 0) + textToInsert = new string('\t', requiredTabs) + textToInsert; + } + else + { + textToInsert = new string(' ', caretColumn); + } + + return textToInsert; + } + + #endregion + + #region Text insertion helpers + + private bool InsertText(string text, bool final) + { + return this.InsertText(text, final, Strings.InsertText, _textView.Caret.OverwriteMode); + } + + private bool InsertText(string text, bool final, string undoText, bool isOverwriteModeEnabled) + { + // Validate + if (text == null) + { + throw new ArgumentNullException("text"); + } + + if ((text.Length == 0) && !final) + { + throw new ArgumentException("Provisional TextInput cannot be zero-length"); + } + + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(undoText)) + { + // Determine undo merge direction(s). By default we allow merging, but in two cases we don't want to merge + // with previous transactions: + // 1. If the document is clean (else you won't be able to undo back to a clean state (Dev10 672382)). + // 2. If we are replacing selected text (replacing selected text should be an atomic undo operation). + var mergeDirections = TextTransactionMergeDirections.Forward | TextTransactionMergeDirections.Backward; + if ((!_textView.Selection.IsEmpty && !IsEmptyBoxSelection()) // replacing selected text. + || (_textDocument != null && !_textDocument.IsDirty)) // document is clean. + { + mergeDirections = TextTransactionMergeDirections.Forward; + } + undoTransaction.MergePolicy = new TextTransactionMergePolicy(mergeDirections); + + this.AddBeforeTextBufferChangePrimitive(); + + IEnumerable<VirtualSnapshotSpan> replaceSpans; + + TextEditAction action = TextEditAction.Type; + + // If this is a non-empty stream selection and *not* an empty box + if (!_textView.Selection.IsEmpty && _immProvisionalComposition == null) + { + // If the caret is in overwrite mode and this is an empty box, treat + // it like an overwrite on each line. + if (_textView.Options.IsOverwriteModeEnabled() && IsEmptyBoxSelection()) + { + List<VirtualSnapshotSpan> spans = new List<VirtualSnapshotSpan>(); + + foreach (var span in _textView.Selection.VirtualSelectedSpans) + { + SnapshotPoint startPoint = span.Start.Position; + + if (span.Start.IsInVirtualSpace || + startPoint.GetContainingLine().End == startPoint) + { + spans.Add(span); + } + else + { + spans.Add(new VirtualSnapshotSpan( + new SnapshotSpan(startPoint, _textView.GetTextElementSpan(startPoint).End))); + } + } + + replaceSpans = spans; + } + else + { + replaceSpans = _textView.Selection.VirtualSelectedSpans; + } + + // The provisional composition span should be null here (the IME should + // finalize before doing anything that could cause a selection) but we treat + // this as a soft error since something time sliced on the UI thread could + // affect the selection. So we simply ignore the old provisional span. + } + else if (_immProvisionalComposition != null) + { + SnapshotSpan provisionalSpan = _immProvisionalComposition.GetSpan(_textView.TextSnapshot); + + if (IsEmptyBoxSelection() && final) + { + // For an empty box, replace the equivalent to the provisional span + // on each line + IList<VirtualSnapshotSpan> spans = new List<VirtualSnapshotSpan>(); + foreach (SnapshotPoint start in _textView.Selection.VirtualSelectedSpans.Select(s => s.Start.Position)) + { + if (start.Position - provisionalSpan.Length >= 0) + { + spans.Add(new VirtualSnapshotSpan( + new SnapshotSpan(start - provisionalSpan.Length, start))); + } + else + { + Debug.Fail("Asked to replace a provisional span that would go past the beginning of the buffer; ignoring."); + } + } + + replaceSpans = spans; + } + else + { + replaceSpans = new VirtualSnapshotSpan[] { + new VirtualSnapshotSpan(provisionalSpan) }; + } + + // The length of the replacement should always be > 0 but there are scenarios + // (such as a replace by some asynchronous command time-sliced to the UI thread) + // that could zero out the span. + + action = TextEditAction.ProvisionalOverwrite; + } + else + { + VirtualSnapshotPoint insertionPoint = _textView.Caret.Position.VirtualBufferPosition; + if (isOverwriteModeEnabled && !insertionPoint.IsInVirtualSpace) + { + SnapshotPoint point = insertionPoint.Position; + replaceSpans = new VirtualSnapshotSpan[] { new VirtualSnapshotSpan( + new SnapshotSpan(point, _textView.GetTextElementSpan(point).End)) }; + } + else + { + replaceSpans = new VirtualSnapshotSpan[] { + new VirtualSnapshotSpan(insertionPoint, insertionPoint) }; + } + } + + ITextVersion currentVersion = _textView.TextSnapshot.Version; + ITrackingSpan newProvisionalSpan = null; + + bool editSuccessful = true; + + int? firstLineInsertedVirtualSpaces = null; + int lastLineInsertedVirtualSpaces = 0; + + ITrackingPoint endPoint = null; + + using (ITextEdit textEdit = _textView.TextBuffer.CreateEdit(EditOptions.None, null, action)) + { + bool firstSpan = true; + foreach (var span in replaceSpans) + { + string lineText = text; + + // Remember the endPoint for a possible provisional span. + // Use negative tracking, so it doesn't move with the newly inserted text. + endPoint = _textView.TextSnapshot.CreateTrackingPoint(span.Start.Position, PointTrackingMode.Negative); + + if (span.Start.IsInVirtualSpace) + { + string whitespace = GetWhitespaceForVirtualSpace(span.Start); + if (firstSpan) + { + //Keep track of the number of whitespace characters -- which might include tabs -- so that the caller can figure out where the inserted text + //actually lands. + _textView.TextBuffer.Properties["WhitespaceInserted"] = whitespace.Length; + } + lineText = whitespace + text; + } + + if (!firstLineInsertedVirtualSpaces.HasValue) + firstLineInsertedVirtualSpaces = lineText.Length - text.Length; + + lastLineInsertedVirtualSpaces = lineText.Length - text.Length; + + if (!textEdit.Replace(span.SnapshotSpan, lineText) || textEdit.Canceled) + { + editSuccessful = false; + break; + } + + firstSpan = false; + } + + if (editSuccessful) + { + textEdit.Apply(); + editSuccessful = !textEdit.Canceled; + } + } + + if (editSuccessful) + { + // Get rid of virtual space if there is any, + // since we've just made it non-virtual + _textView.Caret.MoveTo(_textView.Caret.Position.BufferPosition); + _textView.Selection.Select( + new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position), + new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position)); + + // If the selection ends up being non-empty (meaning not an empty + // single selection *or* an empty box), then clear it. + if (_textView.Selection.VirtualSelectedSpans.Any(s => !s.IsEmpty)) + _textView.Selection.Clear(); + + _textView.Caret.EnsureVisible(); + + this.AddAfterTextBufferChangePrimitive(); + + undoTransaction.Complete(); + + if (final) + { + newProvisionalSpan = null; + } + else if (_textView.Selection.IsReversed) + { + // The active point is at the start. + int virtualSpaces = firstLineInsertedVirtualSpaces.HasValue ? firstLineInsertedVirtualSpaces.Value : 0; + + newProvisionalSpan = currentVersion.Next.CreateTrackingSpan( + new Span(replaceSpans.First().Start.Position + virtualSpaces, text.Length), + SpanTrackingMode.EdgeExclusive); + } + else + { + // The active point is at the end. + // Since text may have been inserted before this point, we need to accommodate the + // amount of inserted text, using the endPoint we constructed earlier. + + int lastSpanStart = endPoint.GetPoint(_textView.TextSnapshot).Position; + + newProvisionalSpan = currentVersion.Next.CreateTrackingSpan( + new Span(lastSpanStart + lastLineInsertedVirtualSpaces, text.Length), + SpanTrackingMode.EdgeExclusive); + } + } + + // This test is for null to non-null or vice versa (two different ITrackingSpans -- even if they cover the same span -- are not + // considered equal). + if (_immProvisionalComposition != newProvisionalSpan) + { + _immProvisionalComposition = newProvisionalSpan; + _textView.ProvisionalTextHighlight = _immProvisionalComposition; + } + + return editSuccessful; + } + } + + public bool InsertTextAsBox(string text, out VirtualSnapshotPoint boxStart, out VirtualSnapshotPoint boxEnd) + { + return InsertTextAsBox(text, out boxStart, out boxEnd, Strings.InsertText); + } + + public bool InsertTextAsBox(string text, out VirtualSnapshotPoint boxStart, out VirtualSnapshotPoint boxEnd, string undoText) + { + if (text == null) + throw new ArgumentNullException("text"); + + boxStart = boxEnd = _textView.Caret.Position.VirtualBufferPosition; + + // You can't use out parameters in a lambda or delegate, so just make a copy of + // boxStart/boxEnd + VirtualSnapshotPoint newStart, newEnd; + newStart = newEnd = boxStart; + + if (text.Length == 0) + return this.InsertText(text); + + Func<bool> action = () => + { + // Separate edit transaction for the delete + if (!DeleteHelper(_textView.Selection.SelectedSpans)) + return false; + + // Put the selection in box mode for this operation + _textView.Selection.Mode = TextSelectionMode.Box; + + _textView.Caret.MoveTo(_textView.Selection.Start); + + // Remember the starting position + VirtualSnapshotPoint oldCaretPos = _textView.Caret.Position.VirtualBufferPosition; + double startX = _textView.Caret.Left; + + // Estimate the number of spaces to the caret, to be used + // for any amount of text that goes past the end of the buffer + int spacesToCaret = _editorPrimitives.View.GetTextPoint(oldCaretPos.Position).DisplayColumn + oldCaretPos.VirtualSpaces; + + ITrackingPoint endPoint = null; + int? firstLineInsertedVirtualSpaces = null; + + using (ITextEdit textEdit = _editorPrimitives.Buffer.AdvancedTextBuffer.CreateEdit()) + { + VirtualSnapshotPoint currentCaret = oldCaretPos; + ITextViewLine currentLine = _textView.GetTextViewLineContainingBufferPosition(currentCaret.Position); + + bool pastEndOfBuffer = false; + + // Read a line at a time from the given text, inserting each line of text + // into the buffer at a successive line below the line the caret is on. + // For any text left over at the end of the buffer, insert endlines as + // well (to create new lines at the end of the file), with virtual space + // for padding. + using (StringReader reader = new StringReader(text)) + { + for (string lineText = reader.ReadLine(); lineText != null;) + { + if (lineText.Length > 0) + { + // Remember the endPoint, for determining the boxEnd argument + endPoint = _textView.TextSnapshot.CreateTrackingPoint(currentCaret.Position, PointTrackingMode.Positive); + + int whitespaceLength = 0; + + // Add on any virtual space needed + if (currentCaret.IsInVirtualSpace) + { + string whitespace = GetWhitespaceForVirtualSpace(currentCaret); + lineText = whitespace + lineText; + whitespaceLength = whitespace.Length; + } + + // Update information about the first inserted line + if (!firstLineInsertedVirtualSpaces.HasValue) + { + firstLineInsertedVirtualSpaces = whitespaceLength; + oldCaretPos = currentCaret; + } + + // Insert the text as part of this edit transaction + if (!textEdit.Insert(currentCaret.Position, lineText)) + return false; + } + + // We've already read past the end of the buffer, so we're done + if (pastEndOfBuffer) + break; + + // If this is the last line in the file, collect the rest + // of the text to insert. + if (currentLine.LineBreakLength == 0) + { + string whitespace = GetWhitespaceForDisplayColumn(spacesToCaret); + string newline = _editorOptions.GetNewLineCharacter(); + + string extraLine = null; + lineText = string.Empty; + string blankLines = string.Empty; + while ((extraLine = reader.ReadLine()) != null) + { + // Either add the line (if there is any text), or + // just add the newline + if (extraLine.Length > 0) + { + lineText += blankLines + // Any blank lines + newline + // a line break, to get a new line + whitespace + // realized virtual space + extraLine; // the line text itself + + blankLines = string.Empty; + } + else + { + blankLines += newline; + } + } + + pastEndOfBuffer = true; + + currentCaret = new VirtualSnapshotPoint(currentLine.EndIncludingLineBreak); + // Current line hasn't changed, so we don't need to set it again + } + else // Otherwise, try and read the next line + { + lineText = reader.ReadLine(); + + if (lineText != null) + { + currentLine = _textView.GetTextViewLineContainingBufferPosition(currentLine.EndIncludingLineBreak); + currentCaret = currentLine.GetInsertionBufferPositionFromXCoordinate(startX); + } + } + } + } + + // If we didn't actually insert any text, then cancel the edit and return false + if (endPoint == null) + { + textEdit.Cancel(); + return false; + } + + textEdit.Apply(); + + if (textEdit.Canceled) + return false; + } + + // Now, figure out the start and end positions + // and update the caret and selection. + _textView.Selection.Clear(); + + // Move the caret back to the starting point, which is now + // no longer in virtual space. + int virtualSpaces = firstLineInsertedVirtualSpaces.HasValue ? firstLineInsertedVirtualSpaces.Value : 0; + + _textView.Caret.MoveTo(new SnapshotPoint(_textView.TextSnapshot, + oldCaretPos.Position.Position + virtualSpaces)); + + newStart = _textView.Caret.Position.VirtualBufferPosition; + + // endPoint was a forward-tracking point at the beginning of the last insertion, + // so it should be positioned at the very end of the inserted text in the new + // snapshot. + newEnd = new VirtualSnapshotPoint(endPoint.GetPoint(_textView.TextSnapshot)); + + return true; + }; + + bool succeeded = ExecuteAction(undoText, action); + + if (succeeded) + { + boxStart = newStart; + boxEnd = newEnd; + } + + return succeeded; + } + #endregion + + #region Clipboard and RTF helpers + + private Func<bool> PrepareClipboardSelectionCopy() + { + NormalizedSnapshotSpanCollection selectedSpans = _textView.Selection.SelectedSpans; + + string text = this.SelectedText; + string rtfText = null; + try + { + rtfText = GenerateRtf(selectedSpans); + } + catch (OperationCanceledException) + { + // Ignore cancellation when doing a copy. The user may not even want RTF text so preventing the normal text from being copied would be overkill. + } + + bool isBox = _textView.Selection.Mode == TextSelectionMode.Box; + + return () => CopyToClipboard(text, rtfText, lineCutCopyTag: false, boxCutCopyTag: isBox); + } + + private Func<bool> PrepareClipboardFullLineCopy(DisplayTextRange textRange) + { + string text = textRange.GetText(); + + string rtfText = null; + try + { + rtfText = GenerateRtf(textRange.AdvancedTextRange); + } + catch (OperationCanceledException) + { + // Ignore cancellation when doing a copy. The user may not even want RTF text so preventing the normal text from being copied would be overkill. + } + + + return () => CopyToClipboard(text, rtfText, lineCutCopyTag: true, boxCutCopyTag: false); + } + + /// <summary> + /// Copies data into the clipboard. + /// </summary> + /// <param name="textData">The textual content to put in the clipboard</param> + /// <param name="rtfData">The RTF formatted data to put in the clipboard</param> + /// <returns>true if operation succeeded.</returns> + private static bool CopyToClipboard(string textData, string rtfData, bool lineCutCopyTag, bool boxCutCopyTag) + { + // note: we should change clipboard data even if textData or rtfData are empty + + // Clipboard may throw exceptions, so enclose Clipboard calls in a try-catch block + try + { + DataObject dataObject = new DataObject(); + + //set plain text format + dataObject.SetText(textData); + + //set any additional data + if (rtfData != null) + { + dataObject.SetData(DataFormats.Rtf, rtfData); + } + + //tag the data in the clipboard if requested + if (lineCutCopyTag) + { + dataObject.SetData(_clipboardLineBasedCutCopyTag, true); + } + + if (boxCutCopyTag) + { + dataObject.SetData(_boxSelectionCutCopyTag, true); + } + + // When adding an item to the clipboard, use delay rendering. We expect the host to flush + // the data down during shutdown if it so desires. Putting the data on the clipboard with delay + // rendering helps with clipboard contention issues with remote desktop and multiple clipboard + // chain listeners. + // WPF, when doing no delay rendering, calls OleSetClipboard and OleFlushClipboard under the covers + // which causes the OS to send out two almost simultaneous clipboard open/close notification pairs + // which confuse applications that try to synchronize clipboard data between multiple machines such + // as MagicMouse or remote desktop. + Clipboard.SetDataObject(dataObject, false); + + return true; + } + catch (System.Runtime.InteropServices.ExternalException) + { + // TODO: Log error + return false; + } + } + + private string GenerateRtf(NormalizedSnapshotSpanCollection spans) + { + //Don't generate RTF for large spans (since it is expensive and probably not wanted). + int length = spans.Sum((span) => span.Length); + if (length < _textView.Options.GetOptionValue(MaxRtfCopyLength.OptionKey)) + { + if (_textView.Options.GetOptionValue(UseAccurateClassificationForRtfCopy.OptionKey)) + { + using (var dialog = WaitHelper.Wait(_factory.WaitIndicator, Strings.WaitTitle, Strings.WaitMessage)) + { + return ((IRtfBuilderService2)(_factory.RtfBuilderService)).GenerateRtf(spans, dialog.CancellationToken); + } + } + else + { + return _factory.RtfBuilderService.GenerateRtf(spans); + } + } + else + return null; + } + + private string GenerateRtf(SnapshotSpan span) + { + return GenerateRtf(new NormalizedSnapshotSpanCollection(span)); + } + + #endregion + + #region Horizontal whitespace helpers + + private bool DeleteHorizontalWhitespace() + { + List<Span> singleSpansToDelete = new List<Span>(); + List<Span> largeSpansToDelete = new List<Span>(); + List<Span> singleTabsToReplace = new List<Span>(); + ITextSnapshot snapshot = _editorPrimitives.View.AdvancedTextView.TextSnapshot; + int whitespaceLocation = -1; + ITrackingPoint lastDeleteLocation = snapshot.CreateTrackingPoint(_editorPrimitives.Caret.CurrentPosition, PointTrackingMode.Positive); + + using (ITextEdit textEdit = snapshot.TextBuffer.CreateEdit()) + { + int startPoint = GetStartPointOfSpanForDeleteHorizontalWhitespace(snapshot); + int endPoint = GetEndPointOfSpanForDeleteHorizontalWhitespace(snapshot); + + for (int i = startPoint; i <= endPoint && i < snapshot.Length; i++) + { + if (IsSpaceCharacter(snapshot[i])) + { + if (whitespaceLocation == -1) + { + whitespaceLocation = i; + } + } + else + { + if (whitespaceLocation != -1) + { + if ((i - whitespaceLocation == 1) && !_editorPrimitives.Selection.IsEmpty) + { + // This is a span of just a single space or tab so track it as such + singleSpansToDelete.Add(Span.FromBounds(whitespaceLocation, i)); + + // If this is a tab character we need to replace it with a space + if (snapshot[i - 1] == '\t') + { + lastDeleteLocation = snapshot.CreateTrackingPoint(i, PointTrackingMode.Positive); + singleTabsToReplace.Add(Span.FromBounds(whitespaceLocation, i)); + } + } + else + { + // This is a span of more than one space or tab so track it as such + lastDeleteLocation = snapshot.CreateTrackingPoint(i, PointTrackingMode.Positive); + largeSpansToDelete.Add(Span.FromBounds(whitespaceLocation, i)); + } + } + whitespaceLocation = -1; + } + } + + // If the last span is at end of the selection + if (whitespaceLocation != -1) + { + lastDeleteLocation = snapshot.CreateTrackingPoint(endPoint, PointTrackingMode.Positive); + if ((endPoint - whitespaceLocation == 1) && !_editorPrimitives.Selection.IsEmpty) + singleSpansToDelete.Add(Span.FromBounds(whitespaceLocation, endPoint)); + else + largeSpansToDelete.Add(Span.FromBounds(whitespaceLocation, endPoint)); + } + + if (!DeleteHorizontalWhitespace(textEdit, largeSpansToDelete, singleSpansToDelete, singleTabsToReplace)) + return false; + + textEdit.Apply(); + + if (textEdit.Canceled) + return false; + + if (_editorPrimitives.Selection.IsEmpty) + { + // Restore the caret to the x-coordinate it was at previously on the new line + _editorPrimitives.Caret.MoveTo(lastDeleteLocation.GetPosition(_editorPrimitives.View.AdvancedTextView.TextSnapshot)); + } + } + + return true; + } + + private static bool DeleteHorizontalWhitespace(ITextEdit textEdit, ICollection<Span> largeSpansToDelete, ICollection<Span> singleSpansToDelete, ICollection<Span> singleTabsToReplace) + { + ITextSnapshot snapshot = textEdit.Snapshot; + Func<int, bool> isLeadingWhitespace = (p) => snapshot.GetLineFromPosition(p).Start == p; + Func<int, bool> isTrailingWhitespace = (p) => snapshot.GetLineFromPosition(p).End == p; + + if (largeSpansToDelete.Count == 0) + { + // If there were no spans of more than one space or tab, + // then delete all spans of just a single space or tab + foreach (var span in singleSpansToDelete) + { + if (!textEdit.Delete(span)) + return false; + } + } + else + { + // If there was at least one span of more than one space or + // tab then replace all of those spans with spaces + foreach (var span in largeSpansToDelete) + { + // if it is leading or trailing whitespace, just delete it + if (isLeadingWhitespace(span.Start) || isTrailingWhitespace(span.End)) + { + if (!textEdit.Delete(span)) + return false; + } + // if there's space adjacent to the beginning/end of the selection + else if (IsSpaceCharacter(snapshot[span.Start - 1]) || IsSpaceCharacter(snapshot[span.End])) + { + if (!textEdit.Delete(span)) + return false; + } + else if (!textEdit.Replace(span, " ")) + return false; + } + + // Replace single tabs with a single space, but + // skip leading / trailing tabs since they will be deleted below by the singleSpansToDelete loop. + foreach (var span in singleTabsToReplace) + { + if (!isLeadingWhitespace(span.Start) && !isTrailingWhitespace(span.End)) + { + if (!textEdit.Replace(span, " ")) + return false; + } + } + + // Delete leading or trailing whitespace in singleSpansToDelete + foreach (var span in singleSpansToDelete) + { + if (isLeadingWhitespace(span.Start) || isTrailingWhitespace(span.End)) + { + if (!textEdit.Delete(span)) + return false; + } + } + } + + return true; + } + + private int GetEndPointOfSpanForDeleteHorizontalWhitespace(ITextSnapshot snapshot) + { + int endPoint = _editorPrimitives.Selection.GetEndPoint().CurrentPosition; + if (_editorPrimitives.Selection.IsEmpty) + { + int endOfLine = _editorPrimitives.Caret.EndOfViewLine; + while (endPoint < endOfLine) + { + if (!IsSpaceCharacter(snapshot[endPoint])) + { + break; + } + endPoint++; + } + } + return endPoint; + } + + private int GetStartPointOfSpanForDeleteHorizontalWhitespace(ITextSnapshot snapshot) + { + int startPoint = _editorPrimitives.Selection.GetStartPoint().CurrentPosition; + if (_editorPrimitives.Selection.IsEmpty) + { + int startOfViewLine = _editorPrimitives.Caret.StartOfViewLine; + while (startPoint > startOfViewLine) + { + // If the caracter *before* the current start point is + // not whitespace, then quit. + if (!IsSpaceCharacter(snapshot[startPoint - 1])) + { + break; + } + + startPoint--; + } + } + return startPoint; + } + + #endregion + + #region Indent/unindent helpers + + // Perform the given indent action (indent/unindent) on each line at the first non-whitespace + // character, skipping lines that are either empty or just whitespace. + // This method is used by Indent, Unindent, IncreaseLineIndent, and DecreaseLineIndent, and + // is essentially a replacement for the editor primitive's Indent and Unindent functions. + private bool PerformIndentActionOnEachBufferLine(Func<SnapshotPoint, ITextEdit, bool> action) + { + Func<ITextEdit, bool> editAction = edit => + { + ITextSnapshot snapshot = _textView.TextSnapshot; + + int startLineNumber = snapshot.GetLineNumberFromPosition(_textView.Selection.Start.Position); + int endLineNumber = snapshot.GetLineNumberFromPosition(_textView.Selection.End.Position); + + for (int i = startLineNumber; i <= endLineNumber; i++) + { + ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i); + + // If the line is blank or the (non-empty) selection ends at the start of this line, exclude + // the line from processing. + if (line.Length == 0 || + (!_textView.Selection.IsEmpty && line.Start == _textView.Selection.End.Position)) + continue; + + TextPoint textPoint = _editorPrimitives.Buffer.GetTextPoint(line.Start).GetFirstNonWhiteSpaceCharacterOnLine(); + + if (textPoint.CurrentPosition == textPoint.EndOfLine) + continue; + + SnapshotPoint point = new SnapshotPoint(snapshot, textPoint.CurrentPosition); + + if (!action(point, edit)) + return false; + } + + return true; + }; + + // Track the start of the selection with PointTrackingMode.Negative through the text + // change. If the result has the same absolute snapshot offset as the point before the change, we'll move + // the selection start point back to there instead of letting it track automatically. + ITrackingPoint startPoint = _textView.TextSnapshot.CreateTrackingPoint(_textView.Selection.Start.Position, PointTrackingMode.Negative); + int startPositionBufferChange = _textView.Selection.Start.Position; + + if (!EditHelper(editAction)) + return false; + + VirtualSnapshotPoint newStart = new VirtualSnapshotPoint(startPoint.GetPoint(_textView.TextSnapshot)); + if (newStart.Position == startPositionBufferChange) + { + bool isReversed = _textView.Selection.IsReversed; + VirtualSnapshotPoint anchor = isReversed ? _textView.Selection.End : newStart; + VirtualSnapshotPoint active = isReversed ? newStart : _textView.Selection.End; + SelectAndMoveCaret(anchor, active); + } + + return true; + } + + // This is used by indent/unindent should be multiline operations. To be multiline, the selection + // points must be on separate lines, and not just an entire line (though not the entire last line, which + // we special case). This is for backwards compatibility with Orcas, but is generally undesirable behavior. + // Dev10 #856382 tracks removing this special behavior for indent and just treating it like a tab at the + // start of the selection. + private bool IndentOperationShouldBeMultiLine + { + get + { + if (_textView.Selection.IsEmpty) + return false; + + var startLine = _textView.Selection.Start.Position.GetContainingLine(); + + bool pointsOnSameLine = _textView.Selection.End.Position <= startLine.End; + + bool lastLineOfFile = startLine.End == startLine.EndIncludingLineBreak; + bool entireLastLineSelected = lastLineOfFile && + _textView.Selection.Start.Position == startLine.Start && + _textView.Selection.End.Position == startLine.End; + + return !pointsOnSameLine || entireLastLineSelected; + } + } + + private bool InsertSingleIndentAtPoint(SnapshotPoint point, ITextEdit edit) + { + VirtualSnapshotPoint virtualPoint = new VirtualSnapshotPoint(point); + VirtualSnapshotSpan span = new VirtualSnapshotSpan(virtualPoint, virtualPoint); + return InsertIndentForSpan(span, edit, exactlyOneIndentLevel: true, useBufferPrimitives: true); + } + + private bool InsertIndentForSpan(VirtualSnapshotSpan span, ITextEdit edit, bool exactlyOneIndentLevel, bool useBufferPrimitives = false) + { + int indentSize = _textView.Options.GetIndentSize(); + bool convertTabsToSpaces = _textView.Options.IsConvertTabsToSpacesEnabled(); + bool boxSelection = _textView.Selection.Mode == TextSelectionMode.Box && + _textView.Selection.Start != _textView.Selection.End; + ITextSnapshot snapshot = edit.Snapshot; + + VirtualSnapshotPoint point = span.Start; + + // In a box selection, we don't insert anything for lines in virtual space + if (boxSelection && point.IsInVirtualSpace) + { + return true; + } + + string textToInsert; + + int startPointForReplace = point.Position; + int endPointForReplace = boxSelection ? startPointForReplace : span.End.Position; + + int currentColumn; + int distanceToNextIndentStop; + + if (!convertTabsToSpaces) + { + // First, move to the left to find the first non-space character + for (; startPointForReplace > 0; startPointForReplace--) + { + if (snapshot[startPointForReplace - 1] != ' ') + break; + } + + // Find the column of the position before the spaces + TextPoint textPoint; + if (useBufferPrimitives) + textPoint = _editorPrimitives.Buffer.GetTextPoint(startPointForReplace); + else + textPoint = _editorPrimitives.View.GetTextPoint(startPointForReplace); + + int startColumn = textPoint.Column; + currentColumn = startColumn + (point.Position - startPointForReplace) + point.VirtualSpaces; + + if (exactlyOneIndentLevel) + distanceToNextIndentStop = indentSize; + else + distanceToNextIndentStop = indentSize - (currentColumn % indentSize); + + int columnToInsertTo = currentColumn + distanceToNextIndentStop; + int totalDistance = columnToInsertTo - startColumn; + + textToInsert = GetWhiteSpaceForPositionAndVirtualSpace(new SnapshotPoint(snapshot, startPointForReplace), totalDistance, useBufferPrimitives); + } + else + { + // Here, we know we don't need to eat spaces + TextPoint textPoint; + if (useBufferPrimitives) + textPoint = _editorPrimitives.Buffer.GetTextPoint(point.Position.Position); + else + textPoint = _editorPrimitives.View.GetTextPoint(point.Position.Position); + + currentColumn = textPoint.Column + point.VirtualSpaces; + + if (exactlyOneIndentLevel) + distanceToNextIndentStop = indentSize; + else + distanceToNextIndentStop = indentSize - (currentColumn % indentSize); + + int columnToInsertTo = currentColumn + distanceToNextIndentStop; + + textToInsert = GetWhiteSpaceForPositionAndVirtualSpace(point.Position, point.VirtualSpaces + distanceToNextIndentStop, useBufferPrimitives); + } + + if (_textView.Caret.OverwriteMode && span.IsEmpty) + { + // Walk forward, towards the end of the line, until we get to the column we will be + // overwriting. + int columnToInsertTo = currentColumn + distanceToNextIndentStop; + + int lineEnd = point.Position.GetContainingLine().End; + + for (; endPointForReplace < lineEnd; endPointForReplace++) + { + if (_editorPrimitives.View.GetTextPoint(endPointForReplace).Column >= columnToInsertTo) + break; + } + } + + return edit.Replace(Span.FromBounds(startPointForReplace, endPointForReplace), textToInsert); + } + + private bool RemoveIndentAtPoint(SnapshotPoint point, ITextEdit edit) + { + return RemoveIndentAtPoint(point, edit, failOnNonWhitespaceCharacter: true, useBufferPrimitives: true); + } + + /// <summary> + /// Remove an "indent" to the left of the given point. If <paramref name="columnsToRemove"/> is specified, remove exactly + /// that amount of indent, instead of the normal logic of finding the previous indent stop. + /// </summary> + private bool RemoveIndentAtPoint(SnapshotPoint point, ITextEdit edit, bool failOnNonWhitespaceCharacter, bool useBufferPrimitives = false, int? columnsToRemove = null) + { + // If we've been asked to remove *no* whitespace, then we're done. + if (columnsToRemove.HasValue && columnsToRemove.Value == 0) + return true; + + // The logic for removing an indent is: + // 1) Find the previous indent level from the span's start position (or use columnsToRemove, if specified). + // 2) Delete characters back to the previous indent level, but + // 3) If the deletion goes past the previous indent level (i.e. by deleting a tab), insert spaces to bring the text + // back to the correct indent level. + + ITextSnapshot snapshot = edit.Snapshot; + + int indentSize = _editorOptions.GetIndentSize(); + + TextPoint startPointAsTextPoint; + if (useBufferPrimitives) + startPointAsTextPoint = _editorPrimitives.Buffer.GetTextPoint(point); + else + startPointAsTextPoint = _editorPrimitives.View.GetTextPoint(point); + + int currentColumn = startPointAsTextPoint.Column; + int startOfLine = startPointAsTextPoint.StartOfLine; + + if (startPointAsTextPoint.CurrentPosition == startOfLine) + return true; + + // Determine the previous indent level; if columnsToRemove is specified, just subtract that + // from our current column. Otherwise, find the previous indent stop. + int previousIndentLevel; + if (columnsToRemove.HasValue) + previousIndentLevel = Math.Max(0, currentColumn - columnsToRemove.Value); + else + previousIndentLevel = ((currentColumn - 1) / indentSize) * indentSize; + + int startPointForReplace = point.Position - 1; + int columnDeletedTo = 0; + + // First, move to the left to find the first non-space character + for (; startPointForReplace >= startOfLine; startPointForReplace--) + { + // First, see if we *can* delete the character + char c = snapshot[startPointForReplace]; + if (c != ' ' && c != '\t') + { + // We can't delete this character, so move forward + startPointForReplace++; + break; + } + + // Now, see if we *should* delete the character + if (useBufferPrimitives) + columnDeletedTo = _editorPrimitives.Buffer.GetTextPoint(startPointForReplace).Column; + else + columnDeletedTo = _editorPrimitives.View.GetTextPoint(startPointForReplace).Column; + + if (columnDeletedTo <= previousIndentLevel) + break; + } + + // If the start point is at or past the span start, there isn't anything to replace. + if (startPointForReplace >= point) + return true; + + // If we can't delete the full indent level and we've been asked to fail in this case, return false. + if (failOnNonWhitespaceCharacter && columnDeletedTo > previousIndentLevel) + return false; + + string textToInsert = string.Empty; + + if (columnDeletedTo < previousIndentLevel) + textToInsert = GetWhiteSpaceForPositionAndVirtualSpace(new SnapshotPoint(snapshot, startPointForReplace), previousIndentLevel - columnDeletedTo, useBufferPrimitives); + + return edit.Replace(Span.FromBounds(startPointForReplace, point.Position), textToInsert); + } + + private void MoveCaretToPreviousIndentStopInVirtualSpace() + { + Debug.Assert(_textView.Caret.InVirtualSpace); + + VirtualSnapshotPoint point = GetPreviousIndentStopInVirtualSpace(_textView.Caret.Position.VirtualBufferPosition); + _textView.Caret.MoveTo(point); + } + + /// <summary> + /// Used by the un-indenting logic to determine what an unindent means in virtual space. + /// </summary> + private VirtualSnapshotPoint GetPreviousIndentStopInVirtualSpace(VirtualSnapshotPoint point) + { + Debug.Assert(point.IsInVirtualSpace); + + // If the point is in virtual space, then just move to the previous indent stop + // NOTE: this is a slight change in behavior from VS9. In VS9, the previous indent stop in + // virtual space would move the caret a indent size worth of characters to the left. + TextPoint textPoint = _editorPrimitives.Buffer.GetTextPoint(point.Position); + int endOfLineColumn = textPoint.Column; + int column = endOfLineColumn + point.VirtualSpaces; + int indentSize = _editorOptions.GetIndentSize(); + int previousIndentSize = ((column - 1) / indentSize) * indentSize; + + if (previousIndentSize > endOfLineColumn) + return new VirtualSnapshotPoint(point.Position, previousIndentSize - endOfLineColumn); + else + return new VirtualSnapshotPoint(point.Position); + } + + #endregion + + #region Box Selection indent/unindent helpers + + /// <summary> + /// Given a "fix-up" anchor/active point determined before the box operation, fix up the current selection's + /// anchor/active point. Either/both points may be null, if no correction is necessary. + /// </summary> + private void FixUpSelectionAfterBoxOperation(VirtualSnapshotPoint? anchorPoint, VirtualSnapshotPoint? activePoint) + { + if (anchorPoint.HasValue || activePoint.HasValue) + { + VirtualSnapshotPoint newAnchor = anchorPoint.HasValue ? anchorPoint.Value.TranslateTo(_textView.TextSnapshot) : + _textView.Selection.AnchorPoint; + VirtualSnapshotPoint newActive = activePoint.HasValue ? activePoint.Value.TranslateTo(_textView.TextSnapshot) : + _textView.Selection.ActivePoint; + SelectAndMoveCaret(newAnchor, newActive, TextSelectionMode.Box, EnsureSpanVisibleOptions.None); + } + } + + /// <summary> + /// If a selection point (anchor or active) is in virtual space, it will need to move after an unindent + /// operation to leave the selection in a proper box. + /// </summary> + /// <returns>The new point to use after the unindent operation, or <c>null</c> if no special move logic + /// will be required.</returns> + private VirtualSnapshotPoint? CalculateBoxUnindentForSelectionPoint(VirtualSnapshotPoint point, int unindentAmount) + { + if (!point.IsInVirtualSpace) + return null; + + var containingLine = _textView.GetTextViewLineContainingBufferPosition(point.Position); + var selectionOnLine = _textView.Selection.GetSelectionOnTextViewLine(containingLine); + + // If the selection on this line isn't in virtual space, it'll track correctly + if (!selectionOnLine.HasValue || !selectionOnLine.Value.Start.IsInVirtualSpace) + return null; + + int spaces = Math.Max(0, point.VirtualSpaces - unindentAmount); + return new VirtualSnapshotPoint(point.Position, spaces); + } + + /// <summary> + /// If a selection point (anchor or active) is in virtual space, it will need to move after an indent + /// operation to leave the selection in a proper box. + /// </summary> + /// <returns>The new point to use after the indent operation, or <c>null</c> if no special move logic + /// will be required.</returns> + private VirtualSnapshotPoint? CalculateBoxIndentForSelectionPoint(VirtualSnapshotPoint point, int indentSize) + { + if (!point.IsInVirtualSpace) + return null; + + TextPoint textPoint = _editorPrimitives.View.GetTextPoint(point.Position.Position); + int column = textPoint.Column + point.VirtualSpaces; + int distanceToNextIndentStop = indentSize - (column % indentSize); + + return new VirtualSnapshotPoint(point.Position, point.VirtualSpaces + distanceToNextIndentStop); + } + + /// <summary> + /// Determine the maximum amount the (current) box selection can be unindented, in columns, so that it will continue + /// to be a box after the indent operation. + /// </summary> + private int DetermineMaxBoxUnindent() + { + // We need to determine the correct amount of whitespace that can be unindented from each line. + // The amount removed should never be larger than the size of an indent, but it should also be consistent, + // meaning that the the smallest amount of whitespace to remove on any line becomes the amount removed + // from every line. + // + // This is *different* from how Orcas/Dev10RTM computed box unindent. These special cased much + // of the logic around moving an indent into the middle of a tab character, which we can now + // correctly handle. + int tabSize = _editorOptions.GetTabSize(); + + // The most we can indent, to start with, is an indentSize worth of indentation + int maxColumnUnindent = _editorOptions.GetIndentSize(); + + foreach (var point in _textView.Selection.VirtualSelectedSpans.Select(s => s.Start)) + { + int columnsToAccountFor = maxColumnUnindent - point.VirtualSpaces; + + if (columnsToAccountFor <= 0) + continue; + + TextPoint textPoint = _editorPrimitives.View.GetTextPoint(point.Position); + int column = textPoint.Column; + + // Walk the line backwards until we run out of columns to account for whitespace + // Note that targetColumn may be negative. + int targetColumn = column - columnsToAccountFor; + + int startOfLine = textPoint.StartOfLine; + + for (int i = point.Position.Position - 1; i >= startOfLine && textPoint.Column >= targetColumn; i--) + { + textPoint.MoveTo(i); + string character = textPoint.GetNextCharacter(); + if (character != " " && character != "\t") + break; + + column = textPoint.Column; + columnsToAccountFor = Math.Max(0, (column - targetColumn)); + + if (column <= targetColumn) + break; + } + + maxColumnUnindent -= columnsToAccountFor; + } + + Debug.Assert(maxColumnUnindent <= _editorOptions.GetIndentSize()); + return maxColumnUnindent; + } + + #endregion + + #region Miscellaneous line helpers + + private DisplayTextRange GetFullLines() + { + DisplayTextRange selectionRange = _editorPrimitives.Selection.Clone(); + DisplayTextPoint startPoint = selectionRange.GetDisplayStartPoint(); + DisplayTextPoint endPoint = selectionRange.GetDisplayEndPoint(); + startPoint.MoveTo(startPoint.StartOfViewLine); + endPoint.MoveToBeginningOfNextViewLine(); + + return startPoint.GetDisplayTextRange(endPoint); + } + + /// <summary> + /// Get the fully visible text line. + /// </summary> + /// <param name="textLine">The first text line to check.</param> + /// <param name="indexOfNextLine">The index of the next text line to check</param> + /// <returns> + /// The text line passed in if it is fully visible, there is only one text line, or the next text line is not fully visible. + /// Otherwise returns the text line at indexOfNextTextLine. + /// </returns> + private ITextViewLine FindFullyVisibleLine(ITextViewLine textLine, int indexOfNextLine) + { + if (textLine.VisibilityState != VisibilityState.FullyVisible) + { + if ((indexOfNextLine >= 0) && (_editorPrimitives.View.AdvancedTextView.TextViewLines.Count > indexOfNextLine)) + { + if (_editorPrimitives.View.AdvancedTextView.TextViewLines[indexOfNextLine].VisibilityState == VisibilityState.FullyVisible) + { + return _editorPrimitives.View.AdvancedTextView.TextViewLines[indexOfNextLine]; + } + } + } + + return textLine; + } + + private void MoveCaretToTextLine(ITextViewLine textLine, bool select) + { + VirtualSnapshotPoint anchor = _textView.Selection.AnchorPoint; + _textView.Caret.MoveTo(textLine); + if (select) + { + _textView.Selection.Select(anchor.TranslateTo(_textView.TextSnapshot), _textView.Caret.Position.VirtualBufferPosition); + } + else + { + _textView.Selection.Clear(); + } + } + + static bool IsPointOnBlankLine(TextPoint textPoint) + { + TextPoint firstTextColumn = textPoint.GetFirstNonWhiteSpaceCharacterOnLine(); + return firstTextColumn.CurrentPosition == textPoint.EndOfLine; + } + + static bool IsPointOnBlankViewLine(DisplayTextPoint displayTextPoint) + { + DisplayTextPoint firstTextColumn = displayTextPoint.GetFirstNonWhiteSpaceCharacterOnViewLine(); + return firstTextColumn.CurrentPosition == displayTextPoint.EndOfViewLine; + } + + #endregion + + #region Tabs <-> spaces + + private bool ConvertSpacesAndTabsHelper(bool toTabs) + { + bool wasEmpty = true; + bool wasReversed = false; + SnapshotSpan conversionRange; + if (_textView.Selection.IsEmpty) + { + conversionRange = _textView.Caret.ContainingTextViewLine.Extent; //This could contain multiple lines due to elisions. + } + else + { + conversionRange = _textView.Selection.StreamSelectionSpan.SnapshotSpan; + wasReversed = _textView.Selection.IsReversed; + wasEmpty = false; + } + + using (ITextEdit textEdit = _textView.TextBuffer.CreateEdit()) + { + SnapshotPoint currentPosition = conversionRange.Start; + int tabSize = _editorOptions.GetTabSize(); + + while (currentPosition < conversionRange.End.Position) + { + //Do the conversion a line at a time since we are using the underlying ITextSnapshotLines to determin column position. + ITextSnapshotLine line = currentPosition.GetContainingLine(); + + int startOfConversion = Math.Max(line.Start.Position, conversionRange.Start.Position); + int endOfConversion = Math.Min(line.End.Position, conversionRange.End.Position); + + //Get a TextPoint so we can use the editor primitives logic for handling multicharacter sequences, etc. + TextPoint point = _editorPrimitives.Buffer.GetTextPoint(startOfConversion); + + int column = point.Column; + + int whiteSpaceStart = point.CurrentPosition; + int whiteSpaceColumnStart = column; + + while (point.CurrentPosition < endOfConversion) + { + char c = textEdit.Snapshot[point.CurrentPosition]; + + if (c == '\t') + { + column = ((column / tabSize) + 1) * tabSize; + } + else + { + if (c != ' ') + { + if (!ConvertTabsAndSpaces(textEdit, toTabs, tabSize, whiteSpaceStart, whiteSpaceColumnStart, point.CurrentPosition, column)) + return false; + } + ++column; + } + + //This handles multicharacter glyphs (at least to the degree that the TextPoint primitive understands them). We want this instead of DisplayTextPoint + //since we do not want this conversion to be affected by elisions or word wrap. + point.MoveToNextCharacter(); + + if ((c != ' ') && (c != '\t')) + { + whiteSpaceStart = point.CurrentPosition; + whiteSpaceColumnStart = column; + } + } + + if (!ConvertTabsAndSpaces(textEdit, toTabs, tabSize, whiteSpaceStart, whiteSpaceColumnStart, point.CurrentPosition, column)) + return false; + + currentPosition = line.EndIncludingLineBreak; + } + + textEdit.Apply(); + + if (textEdit.Canceled) + return false; + } + + if (wasEmpty) + { + _textView.Caret.MoveTo(conversionRange.Start.TranslateTo(_textView.TextSnapshot, PointTrackingMode.Negative)); + } + else + { + _textView.Selection.Select(conversionRange.TranslateTo(_textView.TextSnapshot, SpanTrackingMode.EdgeInclusive), wasReversed); + _textView.Caret.MoveTo(_textView.Selection.ActivePoint); + } + + return true; + } + + private static bool ConvertTabsAndSpaces(ITextEdit textEdit, bool toTabs, int tabSize, int whiteSpaceStart, int whiteSpaceColumnStart, int whiteSpaceEnd, int whiteSpaceColumnEnd) + { + if (whiteSpaceEnd > whiteSpaceStart) + { + Debug.Assert(whiteSpaceColumnEnd > whiteSpaceColumnStart); + + string textToInsert; + if (toTabs) + { + int spacesAfterPreviousTabStop = whiteSpaceColumnEnd % tabSize; + + int columnOfPreviousTabStop = whiteSpaceColumnEnd - spacesAfterPreviousTabStop; + + int requiredTabs = ((columnOfPreviousTabStop - whiteSpaceColumnStart) + tabSize - 1) / tabSize; + if (requiredTabs > 0) + textToInsert = new string('\t', requiredTabs) + new string(' ', spacesAfterPreviousTabStop); + else + textToInsert = new string(' ', whiteSpaceColumnEnd - whiteSpaceColumnStart); + } + else + { + textToInsert = new string(' ', whiteSpaceColumnEnd - whiteSpaceColumnStart); + } + + Span replaceSpan = Span.FromBounds(whiteSpaceStart, whiteSpaceEnd); + if ((replaceSpan.Length != textToInsert.Length) || (textToInsert != textEdit.Snapshot.GetText(replaceSpan))) //performance hack: don't get the text if we know they'll be different. + return textEdit.Replace(replaceSpan, textToInsert); + } + + return true; + } + + #endregion + + #region Edit/Replace/Delete helpers + + internal bool EditHelper(Func<ITextEdit, bool> editAction) + { + using (var edit = _textView.TextBuffer.CreateEdit()) + { + if (!editAction(edit)) + return false; + edit.Apply(); + return !edit.Canceled; + } + } + + internal bool ReplaceHelper(Span span, string text) + { + return EditHelper(e => e.Replace(span, text)); + } + + internal bool ReplaceHelper(VirtualSnapshotSpan virtualSnapshotSpan, string text) + { + if (virtualSnapshotSpan.Start.IsInVirtualSpace) + text = GetWhitespaceForVirtualSpace(virtualSnapshotSpan.Start) + text; + return EditHelper(e => e.Replace(virtualSnapshotSpan.SnapshotSpan, text)); + } + + internal bool ReplaceHelper(NormalizedSpanCollection spans, string text) + { + return EditHelper((e) => + { + foreach (var span in spans) + { + if (!e.Replace(span, text) || e.Canceled) + return false; + } + + return true; + }); + } + + internal bool ReplaceHelper(IEnumerable<VirtualSnapshotSpan> spans, string text) + { + return EditHelper((e) => + { + foreach (var span in spans) + { + string lineText = text; + + if (span.Start.IsInVirtualSpace) + lineText = GetWhitespaceForVirtualSpace(span.Start) + text; + + if (!e.Replace(span.SnapshotSpan, lineText) || e.Canceled) + return false; + } + + return true; + }); + } + + internal bool DeleteHelper(Span span) + { + return EditHelper(e => e.Delete(span)); + } + + internal bool DeleteHelper(NormalizedSpanCollection spans) + { + return EditHelper((e) => + { + foreach (var span in spans) + { + if (!e.Delete(span) || e.Canceled) + return false; + } + + return true; + }); + } + + #endregion + + internal bool IsEmptyBoxSelection() + { + return !_textView.Selection.IsEmpty && + _textView.Selection.VirtualSelectedSpans.All(s => s.IsEmpty); + } + + /// <summary> + /// Position the caret using the smart indent service. Optionally, the caller can choose between + /// using only virtual space (and failing if that isn't possible) or using real spaces when the + /// line is not empty. + /// </summary> + /// <param name="useOnlyVirtualSpace">If <c>true</c>, don't ever use real spaces to position the caret, + /// even if there is no other way to position it with smart indent. Defaults to <c>true</c>.</param> + /// <param name="extendSelection">If <c>true</c>, extend the current selection, from the existing anchor point, + /// to the new caret position.</param> + /// <returns><c>true</c> if the caret was positioned in virtual space.</returns> + private bool PositionCaretWithSmartIndent(bool useOnlyVirtualSpace = true, bool extendSelection = false) + { + var caretPosition = _textView.Caret.Position.VirtualBufferPosition; + var caretLine = caretPosition.Position.GetContainingLine(); + + int? indentation = _factory.SmartIndentationService.GetDesiredIndentation(_textView, caretLine); + if (indentation.HasValue) + { + if (caretPosition.Position == caretLine.End) + { + //Position the caret in virtual space at the appropriate indentation. + var newCaretPoint = new VirtualSnapshotPoint(caretPosition.Position, Math.Max(0, indentation.Value - caretLine.Length)); + var anchorPoint = (extendSelection) ? _textView.Selection.AnchorPoint : newCaretPoint; + SelectAndMoveCaret(anchorPoint, newCaretPoint, selectionMode: TextSelectionMode.Stream, scrollOptions: null); + return true; + } + else if (!useOnlyVirtualSpace) + { + // See how much whitespace already exists in the new line + int existingWhitespaceChars = GetLeadingWhitespaceChars(caretLine, caretPosition.Position); + + // Insert the appropriate amount of leading whitespace, potentially replacing existing whitespace + string whitespace = GetWhiteSpaceForPositionAndVirtualSpace(caretPosition.Position, indentation.Value); + return ReplaceHelper(new SnapshotSpan(caretPosition.Position, existingWhitespaceChars), whitespace); + } + } + + return false; + } + + /// <summary> + /// Returns number of whitespace characters + /// between startPosition and first non-whitespace character in the specified line + /// </summary> + /// <param name="line">Which line to evaluate</param> + /// <param name="startPosition">Position where the count starts</param> + /// <returns>Number of leading whitespace characters located after startPosition</returns> + private int GetLeadingWhitespaceChars(ITextSnapshotLine line, SnapshotPoint startPosition) + { + int whitespace = 0; + for (int i = startPosition.Position; i < line.End; ++i) + { + if (IsSpaceCharacter(line.Snapshot[i])) + { + whitespace++; + } + else + break; + } + return whitespace; + } + + private bool ConvertLeadingWhitespace(string actionName, bool convertTabsToSpaces) + { + Func<bool> action = () => + { + using (ITextEdit textEdit = _editorPrimitives.Buffer.AdvancedTextBuffer.CreateEdit()) + { + ITextSnapshot snapshot = textEdit.Snapshot; + + for (int i = _editorPrimitives.Selection.GetStartPoint().LineNumber; i <= _editorPrimitives.Selection.GetEndPoint().LineNumber; i++) + { + ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i); + + TextPoint startOfLine = _editorPrimitives.Buffer.GetTextPoint(line.Start.Position); + TextPoint firstNonWhitespaceCharacter = startOfLine.GetFirstNonWhiteSpaceCharacterOnLine(); + int columnOfFirstNonWhitespaceCharacter = firstNonWhitespaceCharacter.Column; + + string whitespace = GetWhiteSpaceForPositionAndVirtualSpace(line.Start, columnOfFirstNonWhitespaceCharacter, /* useBufferPrimitives: */ true, convertTabsToSpaces); + + SnapshotSpan currentWhiteSpace = new SnapshotSpan(line.Start, firstNonWhitespaceCharacter.AdvancedTextPoint); + + if (whitespace != currentWhiteSpace.GetText()) + { + if (!textEdit.Replace(currentWhiteSpace, whitespace)) + return false; + } + } + + textEdit.Apply(); + + if (textEdit.Canceled) + return false; + } + + if (_editorPrimitives.Selection.IsEmpty) + { + _editorPrimitives.Caret.MoveTo(_editorPrimitives.Caret.StartOfLine); + } + + return true; + }; + + return ExecuteAction(actionName, action); + } + + /// <summary> + /// Expand a range to include the selection as well, if it is not empty, and return it. + /// </summary> + private TextRange ExpandRangeToIncludeSelection(TextRange range) + { + if (!_editorPrimitives.Selection.IsEmpty) + { + if (range.GetStartPoint().CurrentPosition > _editorPrimitives.Selection.GetStartPoint().CurrentPosition) + { + range.SetStart(_editorPrimitives.Selection.GetStartPoint()); + } + + if (range.GetEndPoint().CurrentPosition < _editorPrimitives.Selection.GetEndPoint().CurrentPosition) + { + range.SetEnd(_editorPrimitives.Selection.GetEndPoint()); + } + } + return range; + } + + private void ScrollByLineAndMoveCaretIfNecessary(ScrollDirection direction) + { + _textView.ViewScroller.ScrollViewportVerticallyByPixels(direction == ScrollDirection.Up ? _textView.LineHeight : (-_textView.LineHeight)); + + //If the caret is not on a fully visible line, move it to the first/last line in the view + ITextViewLine line = _textView.Caret.ContainingTextViewLine; + if (line.VisibilityState != VisibilityState.FullyVisible) + { + //Decide whether or not the caret is above the top or bottom of the view (since it doesn't lie + //on any of the fully visible lines in the middle of the view). + ITextViewLineCollection textLines = _textView.TextViewLines; + ITextViewLine newCaretLine; + + //The end of the first line should be the start of the fully visible lines so use it to decide + //whether the caret is above or below. + ITextViewLine firstVisible = textLines.FirstVisibleLine; + if (_textView.Caret.Position.BufferPosition < firstVisible.EndIncludingLineBreak) + { + //The caret is above the top of the view, move it to the first fully visible line + //(or first partially visible if there is no fully visible line). + newCaretLine = firstVisible; + if (firstVisible.VisibilityState != VisibilityState.FullyVisible) + { + ITextViewLine nextLine = textLines.GetTextViewLineContainingBufferPosition(firstVisible.EndIncludingLineBreak); + if ((nextLine != null) && (nextLine.VisibilityState == VisibilityState.FullyVisible)) + newCaretLine = nextLine; + } + } + else + { + //Otherwise the caret is below the bottom of the view. Move it to the last fully visible line. + ITextViewLine lastVisible = textLines.LastVisibleLine; + newCaretLine = lastVisible; + + if ((lastVisible.VisibilityState != VisibilityState.FullyVisible) && (lastVisible.Start > 0)) + { + ITextViewLine previousLine = textLines.GetTextViewLineContainingBufferPosition(lastVisible.Start - 1); + if ((previousLine != null) && (previousLine.VisibilityState == VisibilityState.FullyVisible)) + newCaretLine = previousLine; + } + } + + // Clear any selections + _textView.Selection.Clear(); + _textView.Caret.MoveTo(newCaretLine); + + //Since the caret is on a fully visible line, this should only scroll horizontally. + _textView.Caret.EnsureVisible(); + } + } + + /// <summary> + /// Determine if the character is a space, which includes everything in + /// the space separator category *plus* tab and a few others (for Orcas parity). + /// </summary> + private static bool IsSpaceCharacter(char c) + { + return c == ' ' || c == '\t' || + (int)c == 0x200B || + char.GetUnicodeCategory(c) == UnicodeCategory.SpaceSeparator; + } + + private bool ExecuteAction(string undoText, Func<bool> action, SelectionUpdate preserveCaretAndSelection = SelectionUpdate.Ignore, bool ensureVisible = false) + { + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(undoText)) + { + ITextSnapshot snapshotBeforeEdit = _textView.TextSnapshot; + + // 1. Before performing the action, add a Before undo primitive and remember the current + // state of the caret/selection + AddBeforeTextBufferChangePrimitive(); + + CaretPosition oldCaretPosition = _textView.Caret.Position; + VirtualSnapshotPoint oldSelectionAnchor = _textView.Selection.AnchorPoint; + VirtualSnapshotPoint oldSelectionActive = _textView.Selection.ActivePoint; + + // 2. Perform the specified action + if (!action()) + return false; + + // 3. Take care of the requested SelectionUpdate + if (preserveCaretAndSelection == SelectionUpdate.Preserve) + { + _textView.Caret.MoveTo(new VirtualSnapshotPoint(new SnapshotPoint(_textView.TextSnapshot, + oldCaretPosition.BufferPosition.Position), + oldCaretPosition.VirtualSpaces), oldCaretPosition.Affinity); + _textView.Selection.Select(new VirtualSnapshotPoint(new SnapshotPoint(_textView.TextSnapshot, + oldSelectionAnchor.Position), + oldSelectionAnchor.VirtualSpaces), + new VirtualSnapshotPoint(new SnapshotPoint(_textView.TextSnapshot, + oldSelectionActive.Position), + oldSelectionActive.VirtualSpaces)); + } + else if (preserveCaretAndSelection == SelectionUpdate.ClearVirtualSpace) + { + _textView.Caret.MoveTo(_textView.Caret.Position.BufferPosition); + _textView.Selection.Select(new VirtualSnapshotPoint(_textView.Selection.AnchorPoint.Position), + new VirtualSnapshotPoint(_textView.Selection.ActivePoint.Position)); + } + else if (preserveCaretAndSelection == SelectionUpdate.ResetUnlessEmptyBox) + { + if (!IsEmptyBoxSelection()) + ResetSelection(); + } + else if (preserveCaretAndSelection == SelectionUpdate.Reset) + { + ResetSelection(); + } + + // 4. If requested, make sure the caret is visible + if (ensureVisible) + _textView.Caret.EnsureVisible(); + + // 5. Finish up the undo transaction + AddAfterTextBufferChangePrimitive(); + + if (snapshotBeforeEdit != _textView.TextSnapshot) + { + undoTransaction.Complete(); + } + } + + return true; + } + + private bool NormalizeLineEndingsHelper(string replacement) + { + // what do we do about projection? may wish to change this to navigate to the document buffer via + // text data model. + ITextBuffer textBuffer = _editorPrimitives.Buffer.AdvancedTextBuffer; + using (ITextEdit edit = textBuffer.CreateEdit()) + { + ITextSnapshot snapshot = edit.Snapshot; + foreach (ITextSnapshotLine line in snapshot.Lines) + { + if (line.LineBreakLength != 0) + { + string breakText = line.GetLineBreakText(); + if (breakText != replacement) + { + if (!edit.Replace(line.End, line.LineBreakLength, replacement)) + return false; + } + } + } + edit.Apply(); + return !edit.Canceled; + } + } + } + + /// <summary> + /// The maximum number of characters copied as rich text to the clipboard. + /// </summary> + /// <remarks> + /// Added as a local class here to avoid needing to patch text.logic (where this would, normally, go). + /// </remarks> + [Export(typeof(EditorOptionDefinition))] + [Name(MaxRtfCopyLength.OptionName)] + public sealed class MaxRtfCopyLength : EditorOptionDefinition<int> + { + public const string OptionName = "MaxRtfCopyLength"; + public static readonly EditorOptionKey<int> OptionKey = new EditorOptionKey<int>(MaxRtfCopyLength.OptionName); + + /// <summary> + /// Gets the default value (10K). + /// </summary> + public override int Default { get { return 10 * 1024; } } + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<int> Key { get { return MaxRtfCopyLength.OptionKey; } } + } + + [Export(typeof(EditorOptionDefinition))] + [Name(UseAccurateClassificationForRtfCopy.OptionName)] + public sealed class UseAccurateClassificationForRtfCopy : EditorOptionDefinition<bool> + { + public const string OptionName = "UseAccurateClassificationForRtfCopy"; + public static readonly EditorOptionKey<bool> OptionKey = new EditorOptionKey<bool>(UseAccurateClassificationForRtfCopy.OptionName); + + public override bool Default { get { return false; } } + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<bool> Key { get { return UseAccurateClassificationForRtfCopy.OptionKey; } } + } +} diff --git a/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs b/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs new file mode 100644 index 0000000..b18e07c --- /dev/null +++ b/src/Text/Impl/EditorOperations/EditorOperationsFactoryService.cs @@ -0,0 +1,89 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.ComponentModel.Composition; + + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Formatting; + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Outlining; + using Microsoft.VisualStudio.Language.Intellisense.Utilities; + + [Export(typeof(IEditorOperationsFactoryService))] + internal sealed class EditorOperationsFactoryService : IEditorOperationsFactoryService + { + [Import] + internal ITextStructureNavigatorSelectorService TextStructureNavigatorFactory { get; set; } + + // This service should be optional: it is implemented on the VS side and other hosts may not implement it. + [Import(AllowDefault = true)] + internal IWaitIndicator WaitIndicator { get; set; } + + [Import] + internal ITextSearchService TextSearchService { get; set; } + + [Import] + internal ITextUndoHistoryRegistry UndoHistoryRegistry { get; set; } + + [Import] + internal ITextBufferUndoManagerProvider TextBufferUndoManagerProvider { get; set; } + + [Import] + internal IEditorPrimitivesFactoryService EditorPrimitivesProvider { get; set; } + + [Import] + internal IEditorOptionsFactoryService EditorOptionsProvider { get; set; } + + [Import] + internal IRtfBuilderService RtfBuilderService { get; set; } + + [Import] + internal ISmartIndentationService SmartIndentationService { get; set; } + + [Import] + internal ITextDocumentFactoryService TextDocumentFactoryService { get; set; } + + [Import] + internal IContentTypeRegistryService ContentTypeRegistryService { get; set; } + + [Import(AllowDefault = true)] + internal IOutliningManagerService OutliningManagerService { get; set; } + + /// <summary> + /// Provides a operations implementation for a given text view. + /// </summary> + /// <param name="textView"> + /// The text view to which the operations will be bound. + /// </param> + /// <returns> + /// An implementation of IEditorOperations that can provide operations implementations for the given text view. + /// </returns> + public IEditorOperations GetEditorOperations(ITextView textView) + { + // Validate + if (textView == null) + { + throw new ArgumentNullException("textView"); + } + + // Only one EditorOperations should be created per ITextView + IEditorOperations editorOperations = null; + + // We create one, only if it doesn't already exist + if (!textView.Properties.TryGetProperty<IEditorOperations>(typeof(EditorOperationsFactoryService), out editorOperations)) + { + editorOperations = new EditorOperations(textView, this); + textView.Properties.AddProperty(typeof(EditorOperationsFactoryService), editorOperations); + } + + return editorOperations; + } + } +} diff --git a/src/Text/Impl/EditorOperations/Strings.Designer.cs b/src/Text/Impl/EditorOperations/Strings.Designer.cs new file mode 100644 index 0000000..a0b6e5d --- /dev/null +++ b/src/Text/Impl/EditorOperations/Strings.Designer.cs @@ -0,0 +1,486 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.VisualStudio.Text.Operations.Implementation { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.UI.Text.EditorOperations.Implementation.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot redo this change.. + /// </summary> + internal static string CannotRedo { + get { + return ResourceManager.GetString("CannotRedo", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot undo this change.. + /// </summary> + internal static string CannotUndo { + get { + return ResourceManager.GetString("CannotUndo", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Capitalize. + /// </summary> + internal static string Capitalize { + get { + return ResourceManager.GetString("Capitalize", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Convert spaces to tabs. + /// </summary> + internal static string ConvertSpacesToTabs { + get { + return ResourceManager.GetString("ConvertSpacesToTabs", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Convert tabs to spaces. + /// </summary> + internal static string ConvertTabsToSpaces { + get { + return ResourceManager.GetString("ConvertTabsToSpaces", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cut line. + /// </summary> + internal static string CutLine { + get { + return ResourceManager.GetString("CutLine", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cut Selection. + /// </summary> + internal static string CutSelection { + get { + return ResourceManager.GetString("CutSelection", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Decrease line indent. + /// </summary> + internal static string DecreaseLineIndent { + get { + return ResourceManager.GetString("DecreaseLineIndent", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete blank lines. + /// </summary> + internal static string DeleteBlankLines { + get { + return ResourceManager.GetString("DeleteBlankLines", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete Character To Left. + /// </summary> + internal static string DeleteCharToLeft { + get { + return ResourceManager.GetString("DeleteCharToLeft", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete Character To Right. + /// </summary> + internal static string DeleteCharToRight { + get { + return ResourceManager.GetString("DeleteCharToRight", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete horizontal white space. + /// </summary> + internal static string DeleteHorizontalWhiteSpace { + get { + return ResourceManager.GetString("DeleteHorizontalWhiteSpace", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete line. + /// </summary> + internal static string DeleteLine { + get { + return ResourceManager.GetString("DeleteLine", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete Text. + /// </summary> + internal static string DeleteText { + get { + return ResourceManager.GetString("DeleteText", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete to BOL. + /// </summary> + internal static string DeleteToBOL { + get { + return ResourceManager.GetString("DeleteToBOL", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete to EOL. + /// </summary> + internal static string DeleteToEOL { + get { + return ResourceManager.GetString("DeleteToEOL", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete Word To Left. + /// </summary> + internal static string DeleteWordToLeft { + get { + return ResourceManager.GetString("DeleteWordToLeft", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Delete Word To Right. + /// </summary> + internal static string DeleteWordToRight { + get { + return ResourceManager.GetString("DeleteWordToRight", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Increase line indent. + /// </summary> + internal static string IncreaseLineIndent { + get { + return ResourceManager.GetString("IncreaseLineIndent", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Indent Selection. + /// </summary> + internal static string IndentSelection { + get { + return ResourceManager.GetString("IndentSelection", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Insert File. + /// </summary> + internal static string InsertFile { + get { + return ResourceManager.GetString("InsertFile", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Insert Final New Line. + /// </summary> + internal static string InsertFinalNewLine { + get { + return ResourceManager.GetString("InsertFinalNewLine", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Insert new line. + /// </summary> + internal static string InsertNewLine { + get { + return ResourceManager.GetString("InsertNewLine", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Insert Tab. + /// </summary> + internal static string InsertTab { + get { + return ResourceManager.GetString("InsertTab", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Insert Text. + /// </summary> + internal static string InsertText { + get { + return ResourceManager.GetString("InsertText", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to MakeLowercase. + /// </summary> + internal static string MakeLowercase { + get { + return ResourceManager.GetString("MakeLowercase", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to MakeUppercase. + /// </summary> + internal static string MakeUppercase { + get { + return ResourceManager.GetString("MakeUppercase", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Move selected lines down. + /// </summary> + internal static string MoveSelLinesDown { + get { + return ResourceManager.GetString("MoveSelLinesDown", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Move selected lines up. + /// </summary> + internal static string MoveSelLinesUp { + get { + return ResourceManager.GetString("MoveSelLinesUp", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Make Line Endings Consistent. + /// </summary> + internal static string NormalizeLineEndings { + get { + return ResourceManager.GetString("NormalizeLineEndings", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Insert new line above. + /// </summary> + internal static string OpenLineAbove { + get { + return ResourceManager.GetString("OpenLineAbove", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Insert new line below. + /// </summary> + internal static string OpenLineBelow { + get { + return ResourceManager.GetString("OpenLineBelow", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Paste. + /// </summary> + internal static string Paste { + get { + return ResourceManager.GetString("Paste", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Remove Previous Tab. + /// </summary> + internal static string RemovePreviousTab { + get { + return ResourceManager.GetString("RemovePreviousTab", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Replace All. + /// </summary> + internal static string ReplaceAll { + get { + return ResourceManager.GetString("ReplaceAll", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Replace Selection With . + /// </summary> + internal static string ReplaceSelectionWith { + get { + return ResourceManager.GetString("ReplaceSelectionWith", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Replace Text. + /// </summary> + internal static string ReplaceText { + get { + return ResourceManager.GetString("ReplaceText", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Tabify selection. + /// </summary> + internal static string Tabify { + get { + return ResourceManager.GetString("Tabify", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Toggle case. + /// </summary> + internal static string ToggleCase { + get { + return ResourceManager.GetString("ToggleCase", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Transpose Character. + /// </summary> + internal static string TransposeCharacter { + get { + return ResourceManager.GetString("TransposeCharacter", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Transpose Line. + /// </summary> + internal static string TransposeLine { + get { + return ResourceManager.GetString("TransposeLine", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Transpose word. + /// </summary> + internal static string TransposeWord { + get { + return ResourceManager.GetString("TransposeWord", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Trim trailing whitespace. + /// </summary> + internal static string TrimTrailingWhitespace { + get { + return ResourceManager.GetString("TrimTrailingWhitespace", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Unindent Selection. + /// </summary> + internal static string UnindentSelection { + get { + return ResourceManager.GetString("UnindentSelection", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Untabify selection. + /// </summary> + internal static string Untabify { + get { + return ResourceManager.GetString("Untabify", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Formatting selection. + /// </summary> + internal static string WaitMessage { + get { + return ResourceManager.GetString("WaitMessage", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Copy. + /// </summary> + internal static string WaitTitle { + get { + return ResourceManager.GetString("WaitTitle", resourceCulture); + } + } + } +} diff --git a/src/Text/Impl/EditorOperations/Strings.resx b/src/Text/Impl/EditorOperations/Strings.resx new file mode 100644 index 0000000..fd8f056 --- /dev/null +++ b/src/Text/Impl/EditorOperations/Strings.resx @@ -0,0 +1,261 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="CannotUndo" xml:space="preserve"> + <value>Cannot undo this change.</value> + </data> + <data name="CannotRedo" xml:space="preserve"> + <value>Cannot redo this change.</value> + </data> + <data name="InsertNewLine" xml:space="preserve"> + <value>Insert new line</value> + </data> + <data name="InsertTab" xml:space="preserve"> + <value>Insert Tab</value> + </data> + <data name="RemovePreviousTab" xml:space="preserve"> + <value>Remove Previous Tab</value> + </data> + <data name="IndentSelection" xml:space="preserve"> + <value>Indent Selection</value> + </data> + <data name="UnindentSelection" xml:space="preserve"> + <value>Unindent Selection</value> + </data> + <data name="InsertText" xml:space="preserve"> + <value>Insert Text</value> + </data> + <data name="DeleteText" xml:space="preserve"> + <value>Delete Text</value> + </data> + <data name="ReplaceSelectionWith" xml:space="preserve"> + <value>Replace Selection With </value> + </data> + <data name="ReplaceText" xml:space="preserve"> + <value>Replace Text</value> + </data> + <data name="ReplaceAll" xml:space="preserve"> + <value>Replace All</value> + </data> + <data name="CutSelection" xml:space="preserve"> + <value>Cut Selection</value> + </data> + <data name="Paste" xml:space="preserve"> + <value>Paste</value> + </data> + <data name="TransposeCharacter" xml:space="preserve"> + <value>Transpose Character</value> + </data> + <data name="TransposeLine" xml:space="preserve"> + <value>Transpose Line</value> + </data> + <data name="MakeLowercase" xml:space="preserve"> + <value>MakeLowercase</value> + </data> + <data name="MakeUppercase" xml:space="preserve"> + <value>MakeUppercase</value> + </data> + <data name="DeleteWordToRight" xml:space="preserve"> + <value>Delete Word To Right</value> + </data> + <data name="DeleteWordToLeft" xml:space="preserve"> + <value>Delete Word To Left</value> + </data> + <data name="DeleteCharToLeft" xml:space="preserve"> + <value>Delete Character To Left</value> + </data> + <data name="DeleteCharToRight" xml:space="preserve"> + <value>Delete Character To Right</value> + </data> + <data name="InsertFile" xml:space="preserve"> + <value>Insert File</value> + </data> + <data name="Capitalize" xml:space="preserve"> + <value>Capitalize</value> + </data> + <data name="ToggleCase" xml:space="preserve"> + <value>Toggle case</value> + </data> + <data name="DeleteLine" xml:space="preserve"> + <value>Delete line</value> + </data> + <data name="CutLine" xml:space="preserve"> + <value>Cut line</value> + </data> + <data name="DeleteToBOL" xml:space="preserve"> + <value>Delete to BOL</value> + </data> + <data name="DeleteToEOL" xml:space="preserve"> + <value>Delete to EOL</value> + </data> + <data name="OpenLineAbove" xml:space="preserve"> + <value>Insert new line above</value> + </data> + <data name="OpenLineBelow" xml:space="preserve"> + <value>Insert new line below</value> + </data> + <data name="TransposeWord" xml:space="preserve"> + <value>Transpose word</value> + </data> + <data name="IncreaseLineIndent" xml:space="preserve"> + <value>Increase line indent</value> + </data> + <data name="DecreaseLineIndent" xml:space="preserve"> + <value>Decrease line indent</value> + </data> + <data name="DeleteBlankLines" xml:space="preserve"> + <value>Delete blank lines</value> + </data> + <data name="DeleteHorizontalWhiteSpace" xml:space="preserve"> + <value>Delete horizontal white space</value> + </data> + <data name="Tabify" xml:space="preserve"> + <value>Tabify selection</value> + </data> + <data name="Untabify" xml:space="preserve"> + <value>Untabify selection</value> + </data> + <data name="ConvertTabsToSpaces" xml:space="preserve"> + <value>Convert tabs to spaces</value> + </data> + <data name="ConvertSpacesToTabs" xml:space="preserve"> + <value>Convert spaces to tabs</value> + </data> + <data name="NormalizeLineEndings" xml:space="preserve"> + <value>Make Line Endings Consistent</value> + </data> + <data name="MoveSelLinesUp" xml:space="preserve"> + <value>Move selected lines up</value> + </data> + <data name="MoveSelLinesDown" xml:space="preserve"> + <value>Move selected lines down</value> + </data> + <data name="WaitMessage" xml:space="preserve"> + <value>Formatting selection</value> + </data> + <data name="WaitTitle" xml:space="preserve"> + <value>Copy</value> + </data> + <data name="InsertFinalNewLine" xml:space="preserve"> + <value>Insert Final New Line</value> + </data> + <data name="TrimTrailingWhitespace" xml:space="preserve"> + <value>Trim trailing whitespace</value> + </data> +</root> diff --git a/src/Text/Impl/EditorOperations/TextEditAction.cs b/src/Text/Impl/EditorOperations/TextEditAction.cs new file mode 100644 index 0000000..c5b3f54 --- /dev/null +++ b/src/Text/Impl/EditorOperations/TextEditAction.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + /// <summary> + /// Enum value stating type of text edit action. + /// </summary> + internal enum TextEditAction + { + None, + Type, + Delete, + Backspace, + Paste, + Enter, + AutoIndent, + Replace, + ProvisionalOverwrite + } +} diff --git a/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs b/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs new file mode 100644 index 0000000..d913c0d --- /dev/null +++ b/src/Text/Impl/EditorOperations/TextTransactionMergePolicy.cs @@ -0,0 +1,140 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.Linq; + using System.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text; + using System.Diagnostics; + + [Flags] + internal enum TextTransactionMergeDirections + { + Forward = 0x0001, + Backward = 0x0002 + } + + /// <summary> + /// This is the merge policy used for determining whether text's undo transactions can be merged. + /// </summary> + internal class TextTransactionMergePolicy : IMergeTextUndoTransactionPolicy + { + #region Private members + TextTransactionMergeDirections _allowableMergeDirections; + #endregion + + #region Constructors + public TextTransactionMergePolicy() : this (TextTransactionMergeDirections.Forward | TextTransactionMergeDirections.Backward) + { + } + + public TextTransactionMergePolicy(TextTransactionMergeDirections allowableMergeDirections) + { + _allowableMergeDirections = allowableMergeDirections; + } + #endregion + + #region IMergeTextUndoTransactionPolicy Members + + public bool CanMerge(ITextUndoTransaction newTransaction, ITextUndoTransaction oldTransaction) + { + // Validate + if (newTransaction == null) + { + throw new ArgumentNullException("newTransaction"); + } + + if (oldTransaction == null) + { + throw new ArgumentNullException("oldTransaction"); + } + + TextTransactionMergePolicy oldPolicy = oldTransaction.MergePolicy as TextTransactionMergePolicy; + TextTransactionMergePolicy newPolicy = newTransaction.MergePolicy as TextTransactionMergePolicy; + if (oldPolicy == null || newPolicy == null) + { + throw new InvalidOperationException("The MergePolicy for both transactions should be a TextTransactionMergePolicy."); + } + + // Make sure the merge policy directions permit merging these two transactions. + if ((oldPolicy._allowableMergeDirections & TextTransactionMergeDirections.Forward) == 0 || + (newPolicy._allowableMergeDirections & TextTransactionMergeDirections.Backward) == 0) + { + return false; + } + + // Only merge text transactions that have the same description + if (newTransaction.Description != oldTransaction.Description) + { + return false; + } + + // If one of the transactions is empty, than it is safe to merge + if ((newTransaction.UndoPrimitives.Count == 0) || (oldTransaction.UndoPrimitives.Count == 0)) + { + return true; + } + + // Make sure that we only merge consecutive edits + ITextUndoPrimitive newerBeforeTextChangePrimitive = newTransaction.UndoPrimitives[0]; + ITextUndoPrimitive olderAfterTextChangePrimitive = oldTransaction.UndoPrimitives[oldTransaction.UndoPrimitives.Count - 1]; + + return newerBeforeTextChangePrimitive.CanMerge(olderAfterTextChangePrimitive); + } + + public void PerformTransactionMerge(ITextUndoTransaction existingTransaction, ITextUndoTransaction newTransaction) + { + if (existingTransaction == null) + throw new ArgumentNullException("existingTransaction"); + if (newTransaction == null) + throw new ArgumentNullException("newTransaction"); + + // Remove trailing AfterTextBufferChangeUndoPrimitive from previous transaction and skip copying + // initial BeforeTextBufferChangeUndoPrimitive from newTransaction, as they are unnecessary. + int copyStartIndex = 0; + int existingCount = existingTransaction.UndoPrimitives.Count; + int newCount = newTransaction.UndoPrimitives.Count; + if (existingCount > 0 && + newCount > 0 && + existingTransaction.UndoPrimitives[existingCount - 1] is AfterTextBufferChangeUndoPrimitive && + newTransaction.UndoPrimitives[0] is BeforeTextBufferChangeUndoPrimitive) + { + existingTransaction.UndoPrimitives.RemoveAt(existingCount - 1); + copyStartIndex = 1; + } + else + { + // Unless undo is disabled (in which case both transactions will be empty), this is unexpected. + Debug.Assert(existingCount == 0 && newCount == 0, + "Expected previous transaction to end with AfterTextBufferChangeUndoPrimitive and " + + "new transaction to start with BeforeTextBufferChangeUndoPrimitive"); + } + + // Copy items from newTransaction into existingTransaction. + for (int i = copyStartIndex; i < newTransaction.UndoPrimitives.Count; i++) + { + existingTransaction.UndoPrimitives.Add(newTransaction.UndoPrimitives[i]); + } + } + + public bool TestCompatiblePolicy(IMergeTextUndoTransactionPolicy other) + { + if (other == null) + { + throw new ArgumentNullException("other"); + } + + // Only merge transaction if they are both a text transaction + return this.GetType() == other.GetType(); + } + + #endregion + } +} diff --git a/src/Text/Impl/EditorOptions/EditorOptions.cs b/src/Text/Impl/EditorOptions/EditorOptions.cs new file mode 100644 index 0000000..59a6e55 --- /dev/null +++ b/src/Text/Impl/EditorOptions/EditorOptions.cs @@ -0,0 +1,360 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.Specialized; + using System.Linq; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + + internal class EditorOptions : IEditorOptions + { + IPropertyOwner Scope { get; set; } + + HybridDictionary OptionsSetLocally { get; set; } + + private EditorOptionsFactoryService _factory; + + FrugalList<WeakReference> DerivedEditorOptions = new FrugalList<WeakReference>(); + + internal EditorOptions(EditorOptions parent, + IPropertyOwner scope, + EditorOptionsFactoryService factory) + { + _parent = parent; + _factory = factory; + this.Scope = scope; + + this.OptionsSetLocally = new HybridDictionary(); + + if (parent != null) + { + parent.AddDerivedOptions(this); + } + } + + #region IEditorOptions Members + + private EditorOptions _parent; + public IEditorOptions Parent + { + get + { + return _parent; + } + set + { + if (_parent == value) + return; + + // _parent == null => this is the global options instance + if (_parent == null) + throw new InvalidOperationException("Cannot change the Parent of the global options."); + + if (value == null) + throw new ArgumentNullException("value"); + + if (value == this) + throw new ArgumentException("The Parent of this instance of IEditorOptions cannot be set to itself."); + + EditorOptions newParent = value as EditorOptions; + + if (newParent == null) + throw new ArgumentException("New parent must be an instance of IEditorOptions generated by the same factory as this instance."); + + var oldParent = _parent; + + _parent.RemovedDerivedOptions(this); + _parent = newParent; + _parent.AddDerivedOptions(this); + + this.CheckForCycle(); + + // TODO: Should we be more specific? Should there be a + // version of OptionsChanged that says "everything has changed"? + + // Send out an event for each supported option that isn't already + // set locally (since the update in parent won't change the + // observed value). + foreach (var definition in _factory.GetInstantiatedOptions(this.Scope)) + { + if (!this.OptionsSetLocally.Contains(definition.Name)) + { + object oldValue = oldParent.GetOptionForChild(definition); + object newValue = _parent.GetOptionForChild(definition); + + if (!object.Equals(oldValue, newValue)) + RaiseChangedEvent(definition); + } + } + } + } + + public T GetOptionValue<T>(string optionId) + { + var definition = _factory.GetOptionDefinitionOrThrow(optionId); + + if (!typeof(T).IsAssignableFrom(definition.ValueType)) + throw new InvalidOperationException("Invalid type requested for the given option."); + + object value = this.GetOptionValue(definition); + return (T)value; + } + + public T GetOptionValue<T>(EditorOptionKey<T> key) + { + return GetOptionValue<T>(key.Name); + } + + public object GetOptionValue(string optionId) + { + return GetOptionValue(_factory.GetOptionDefinitionOrThrow(optionId)); + } + + private object GetOptionValue(EditorOptionDefinition definition) + { + object value; + + if (!TryGetOption(definition, out value)) + throw new ArgumentException(string.Format("The specified option is not valid in this scope: {0}", definition.Name)); + + return value; + } + + public void SetOptionValue(string optionId, object value) + { + EditorOptionDefinition definition = _factory.GetOptionDefinitionOrThrow(optionId); + + // Make sure the type of the provided value is correct + if (!definition.ValueType.IsAssignableFrom(value.GetType())) + { + throw new ArgumentException("Specified option value is of an invalid type", "value"); + } + // Make sure the option is valid, also + else if(!definition.IsValid(ref value)) + { + throw new ArgumentException("The supplied value failed validation for the option.", "value"); + } + // Finally, set the option value locally + else + { + object currentValue = this.GetOptionValue(definition); + OptionsSetLocally[optionId] = value; + + if (!object.Equals(currentValue, value)) + { + RaiseChangedEvent(definition); + } + } + } + + public void SetOptionValue<T>(EditorOptionKey<T> key, T value) + { + SetOptionValue(key.Name, value); + } + + public bool IsOptionDefined(string optionId, bool localScopeOnly) + { + if (localScopeOnly && (_parent != null)) //All options with valid definitions are set for the root. + return OptionsSetLocally.Contains(optionId); + + EditorOptionDefinition definition = _factory.GetOptionDefinition(optionId); + if ((definition != null) && + (Scope == null || definition.IsApplicableToScope(Scope))) + { + return true; + } + + return false; + } + + public bool IsOptionDefined<T>(EditorOptionKey<T> key, bool localScopeOnly) + { + if (localScopeOnly && (_parent != null)) //All options with valid definitions are set for the root. + { + return OptionsSetLocally.Contains(key.Name); + } + + EditorOptionDefinition definition = _factory.GetOptionDefinition(key.Name); + if ((definition != null) && + (Scope == null || definition.IsApplicableToScope(Scope)) && + definition.ValueType.IsEquivalentTo(typeof(T))) + { + return true; + } + + return false; + } + + public bool ClearOptionValue(string optionId) + { + if (this.Parent == null) + { + // Can't clear options on the Global options + return false; + } + + if (OptionsSetLocally.Contains(optionId)) + { + object currentValue = OptionsSetLocally[optionId]; + + OptionsSetLocally.Remove(optionId); + + EditorOptionDefinition definition = _factory.GetOptionDefinitionOrThrow(optionId); + object inheritedValue = this.GetOptionValue(definition); + + // See what the inherited option value was. If it isn't changing, + // then we don't need to raise an event. + if (!object.Equals(currentValue, inheritedValue)) + { + RaiseChangedEvent(definition); + } + + return true; + } + + return false; + } + + public bool ClearOptionValue<T>(EditorOptionKey<T> key) + { + return ClearOptionValue(key.Name); + } + + public IEnumerable<EditorOptionDefinition> SupportedOptions + { + get + { + return _factory.GetSupportedOptions(this.Scope); + } + } + + public IEditorOptions GlobalOptions + { + get + { + return _factory.GlobalOptions; + } + } + + public event EventHandler<EditorOptionChangedEventArgs> OptionChanged; + + #endregion + + #region Private Helpers + + //A hook so we can tell whether or not the options have been hooked. Used only by unit tests. + private object OptionChangedValue { get { return this.OptionChanged; } } + + private void RaiseChangedEvent(EditorOptionDefinition definition) + { + // First, send out local events, but only if the change is valid in this scope + if (Scope == null || definition.IsApplicableToScope(Scope)) + { + var tempEvent = OptionChanged; + if (tempEvent != null) + tempEvent(this, new EditorOptionChangedEventArgs(definition.Name)); + } + + // Get rid of the expired refs + DerivedEditorOptions.RemoveAll(weakref => !weakref.IsAlive); + + // Now, notify a copy of the derived options (since an event might modify the DerivedEditorOptions). + foreach (var weakRef in new FrugalList<WeakReference>(DerivedEditorOptions)) + { + EditorOptions derived = weakRef.Target as EditorOptions; + if (derived != null) + derived.OnParentOptionChanged(definition); + } + } + + private void CheckForCycle() + { + EditorOptions parent = _parent; + HashSet<EditorOptions> visited = new HashSet<EditorOptions>(); + + while (parent != null) + { + if (visited.Contains(parent)) + throw new ArgumentException("Cycles are not allowed in the Parent chain."); + + visited.Add(parent); + parent = parent._parent; + } + } + + #endregion + + #region Internal "event" handling + + internal void AddDerivedOptions(EditorOptions derived) + { + // Get rid of the expired refs + DerivedEditorOptions.RemoveAll(weakref => !weakref.IsAlive); + + DerivedEditorOptions.Add(new WeakReference(derived)); + } + + internal void RemovedDerivedOptions(EditorOptions derived) + { + foreach (var weakRef in DerivedEditorOptions) + { + if (weakRef.Target == derived) + { + DerivedEditorOptions.Remove(weakRef); + break; + } + } + } + + internal void OnParentOptionChanged(EditorOptionDefinition definition) + { + // We only notify if the given option isn't already set locally, since it + // would be overriden by a parent option changing. + if (!this.OptionsSetLocally.Contains(definition.Name)) + RaiseChangedEvent(definition); + } + + #endregion + + private bool TryGetOption(EditorOptionDefinition definition, out object value) + { + value = null; + + if (Scope != null && !definition.IsApplicableToScope(Scope)) + return false; + + value = this.GetOptionForChild(definition); + return true; + } + + /// <summary> + /// Get the given option from this (or its ancestors). The caller should have already checked to ensure + /// the given option is valid in the scope being requested. + /// </summary> + /// <param name="definition">Definition of the option to find.</param> + /// <returns>The option's current value.</returns> + internal object GetOptionForChild(EditorOptionDefinition definition) + { + if (OptionsSetLocally.Contains(definition.Name)) + { + return OptionsSetLocally[definition.Name]; + } + + if (_parent == null) + { + return definition.DefaultValue; + } + + return _parent.GetOptionForChild(definition); + } + } +} diff --git a/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs new file mode 100644 index 0000000..733450b --- /dev/null +++ b/src/Text/Impl/EditorOptions/EditorOptionsFactoryService.cs @@ -0,0 +1,185 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorOptions.Implementation +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.ComponentModel.Composition; + using System.Globalization; + using System.Linq; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + + [Export(typeof(IEditorOptionsFactoryService))] + internal sealed class EditorOptionsFactoryService : IEditorOptionsFactoryService + { + [ImportMany(typeof(EditorOptionDefinition))] + internal List<Lazy<EditorOptionDefinition, INameMetadata>> OptionImports { get; set; } + + private EditorOptions _globalOptions; + private IDictionary<string, EditorOptionDefinition> _instantiatedOptionDefinitions = new Dictionary<string, EditorOptionDefinition>(); + private IDictionary<string, Lazy<EditorOptionDefinition, INameMetadata>> _namedOptionImports = new Dictionary<string, Lazy<EditorOptionDefinition, INameMetadata>>(); + + [Import] + internal GuardedOperations guardedOperations = null; + + #region IEditorOptionsFactoryService Members + + public IEditorOptions GetOptions(IPropertyOwner scope) + { + if (scope == null) + throw new ArgumentNullException("scope"); + + return scope.Properties.GetOrCreateSingletonProperty<IEditorOptions>(() => new EditorOptions(this.GlobalOptions as EditorOptions, scope, this)); + } + + public IEditorOptions CreateOptions() + { + return new EditorOptions(this.GlobalOptions as EditorOptions, null, this); + } + + public IEditorOptions GlobalOptions + { + get + { + if (_globalOptions == null) + { + //We're guranteed that the first thing that happens when anyone tries to create options is that the global options will be created first, + //so do initialization here. + _globalOptions = new EditorOptions(null, null, this); + + //Initialize _after_ setting _globalOptions so that -- since this is a property -- we will only be initialized once if stepping through + //this code in the debugger (and trying to evaluate the .GlobalOptions in the watch window. + this.Initialize(); + } + + return _globalOptions; + } + } + #endregion + + private void Initialize() + { + //Don't need to start locking things (yet) + foreach (var import in this.OptionImports) + { + if (import.Metadata.Name != null) + { + //The external user kindly provided a name as metadata. + this.SafeAdd(_namedOptionImports, import.Metadata.Name, import); + +#if DEBUG + if (import.Metadata.Name.Contains('\\')) + System.Diagnostics.Debug.WriteLine("option with \\ " + import.Metadata.Name); +#endif + } + else + { + //They didn't so we need to instantiate the extension in order to discover the name. + var definition = this.guardedOperations.InstantiateExtension(import, import); + + this.SafeAdd(_instantiatedOptionDefinitions, definition.Name, definition); + + +#if DEBUG + System.Diagnostics.Debug.WriteLine("unnamed option: " + definition.Name); +#endif + } + } + } + + private void SafeAdd<T>(IDictionary<string, T> dictionary, string name, T value) + { + try + { + dictionary.Add(name, value); + } + catch (ArgumentException) + { + this.guardedOperations.HandleException(this, + new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Duplicate EditorOptionDefinition named {0}", name))); + } + } + + internal EditorOptionDefinition GetOptionDefinition(string optionId) + { + lock (_instantiatedOptionDefinitions) + { + EditorOptionDefinition definition; + if (!_instantiatedOptionDefinitions.TryGetValue(optionId, out definition)) + { + Lazy<EditorOptionDefinition, INameMetadata> import; + if (_namedOptionImports.TryGetValue(optionId, out import)) + { + definition = this.guardedOperations.InstantiateExtension(import, import); + + _namedOptionImports.Remove(optionId); + _instantiatedOptionDefinitions.Add(optionId, definition); + } + } + + return definition; + } + } + + internal EditorOptionDefinition GetOptionDefinitionOrThrow(string optionId) + { + var definition = this.GetOptionDefinition(optionId); + if (definition == null) + throw new ArgumentException(string.Format("No EditorOptionDefinition export found for the given option name: {0}", optionId), "optionId"); + + return definition; + } + + internal IEnumerable<EditorOptionDefinition> GetSupportedOptions(IPropertyOwner scope) + { + //Unfortunately, to make this work, we need to instantiate everything. Do it immediately so that + //if someone does something like nesting calls to SupportedOptions we will have something stable. + lock (_instantiatedOptionDefinitions) + { + foreach (var import in _namedOptionImports) + { + var definition = this.guardedOperations.InstantiateExtension(import.Value, import.Value); + this.SafeAdd(_instantiatedOptionDefinitions, import.Key, definition); //Use the name from the metadata, not the name from the definition. + } + + _namedOptionImports.Clear(); + } + + //At this point, _instantiatedOptionDefinitions should never change so we don't need to lock/copy. + foreach (var definition in _instantiatedOptionDefinitions.Values) + { + if ((scope == null) || definition.IsApplicableToScope(scope)) + yield return definition; + } + } + + internal IEnumerable<EditorOptionDefinition> GetInstantiatedOptions(IPropertyOwner scope) + { + List<EditorOptionDefinition> definitions; + lock(_instantiatedOptionDefinitions) + { + definitions = _instantiatedOptionDefinitions.Values.ToList(); + } + + foreach (var definition in definitions) + { + if ((scope == null) || definition.IsApplicableToScope(scope)) + yield return definition; + } + } + } + + public interface INameMetadata + { + [DefaultValue(null)] + string Name { get; } + } +} diff --git a/src/Text/Impl/EditorOptions/TextModelEditorOptions.cs b/src/Text/Impl/EditorOptions/TextModelEditorOptions.cs new file mode 100644 index 0000000..55c0c89 --- /dev/null +++ b/src/Text/Impl/EditorOptions/TextModelEditorOptions.cs @@ -0,0 +1,139 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Editor +{ + /// <summary> + /// Internal options for text storage tuning. + /// </summary> + public static class TextModelEditorOptions + { + /// <summary> + /// The default option that determines the file size above which compressed storage will be used. + /// </summary> + public static readonly EditorOptionKey<int> CompressedStorageFileSizeThresholdOptionId = new EditorOptionKey<int>(CompressedStorageFileSizeThresholdOptionName); + public const string CompressedStorageFileSizeThresholdOptionName = "CompressedStorageFileSizeThreshold"; + + /// <summary> + /// The default option that determines the page size when compressed storage is in use. + /// </summary> + public static readonly EditorOptionKey<int> CompressedStoragePageSizeOptionId = new EditorOptionKey<int>(CompressedStoragePageSizeOptionName); + public const string CompressedStoragePageSizeOptionName = "CompressedStoragePageSize"; + + /// <summary> + /// The default option that specifies how many pages will be retained in memory when compressed storage is in use. + /// </summary> + public static readonly EditorOptionKey<int> CompressedStorageMaxLoadedPagesOptionId = new EditorOptionKey<int>(CompressedStorageMaxLoadedPagesOptionName); + public const string CompressedStorageMaxLoadedPagesOptionName = "CompressedStorageMaxLoadedPages"; + + /// <summary> + /// The default option that determines whether weak references to discarded pages will be retained. + /// </summary> + public static readonly EditorOptionKey<bool> CompressedStorageRetainWeakReferencesOptionId = new EditorOptionKey<bool>(CompressedStorageRetainWeakReferencesOptionName); + public const string CompressedStorageRetainWeakReferencesOptionName = "CompressedStorageRetainWeakReferences"; + + /// <summary> + /// The default option that determines the size above which differencing of text changes will not be attempted even when requested. + /// </summary> + public static readonly EditorOptionKey<int> DiffSizeThresholdOptionId = new EditorOptionKey<int>(DiffSizeThresholdOptionName); + public const string DiffSizeThresholdOptionName = "DiffSizeThreshold"; + } + + /// <summary> + /// The option definition that determines the file size above which compressed storage will be used. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(TextModelEditorOptions.CompressedStorageFileSizeThresholdOptionName)] + public sealed class CompressedStorageFileSizeThreshold : EditorOptionDefinition<int> + { + /// <summary> + /// Gets the default value (5 megabytes) + /// </summary> + public override int Default { get { return 1024 * 1024 * 5; } } + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<int> Key { get { return TextModelEditorOptions.CompressedStorageFileSizeThresholdOptionId; } } + } + + /// <summary> + /// The option definition that determines the page size when compressed storage is in use. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(TextModelEditorOptions.CompressedStoragePageSizeOptionName)] + public sealed class CompressedStoragePageSize : EditorOptionDefinition<int> + { + /// <summary> + /// Gets the default value (1 megabyte) + /// </summary> + public override int Default { get { return 1024 * 1024; } } + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<int> Key { get { return TextModelEditorOptions.CompressedStoragePageSizeOptionId; } } + } + + /// <summary> + /// The default option that specifies how many pages will be retained in memory when compressed storage is in use. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(TextModelEditorOptions.CompressedStorageMaxLoadedPagesOptionName)] + public sealed class CompressedStorageMaxLoadedPages : EditorOptionDefinition<int> + { + /// <summary> + /// Gets the default value (3) + /// </summary> + public override int Default { get { return 3; } } + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<int> Key { get { return TextModelEditorOptions.CompressedStorageMaxLoadedPagesOptionId; } } + } + + /// <summary> + /// The option definition that determines whether weak references to discarded pages will be retained. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(TextModelEditorOptions.CompressedStorageRetainWeakReferencesOptionName)] + public sealed class CompressedStorageRetainWeakReferences : EditorOptionDefinition<bool> + { + /// <summary> + /// Gets the default value (true) + /// </summary> + public override bool Default { get { return true; } } + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<bool> Key { get { return TextModelEditorOptions.CompressedStorageRetainWeakReferencesOptionId; } } + } + + /// <summary> + /// The default option that determines the size above which differencing of text changes will not be attempted even when requested. + /// </summary> + [Export(typeof(EditorOptionDefinition))] + [Name(TextModelEditorOptions.DiffSizeThresholdOptionName)] + public sealed class DiffSizeThreshold : EditorOptionDefinition<int> + { + /// <summary> + /// Gets the default value (10 MB) + /// </summary> + public override int Default { get { return 25 * 1024 * 1024; } } + + /// <summary> + /// Gets the editor option key. + /// </summary> + public override EditorOptionKey<int> Key { get { return TextModelEditorOptions.DiffSizeThresholdOptionId; } } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/EditorOptions/TextModelOptionsSetter.cs b/src/Text/Impl/EditorOptions/TextModelOptionsSetter.cs new file mode 100644 index 0000000..65b814d --- /dev/null +++ b/src/Text/Impl/EditorOptions/TextModelOptionsSetter.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Editor.Implementation +{ + [Export(typeof(ITextModelOptionsSetter))] + public class TextModelOptionsSetter : ITextModelOptionsSetter + { + /// <summary> + /// Propagate options from editor options to the text model (editor options are not visible at the text model level). + /// </summary> + /// <param name="options"></param> + public void SetTextModelOptions(IEditorOptions options) + { + TextModelOptions.CompressedStorageFileSizeThreshold = options.GetOptionValue(TextModelEditorOptions.CompressedStorageFileSizeThresholdOptionId); + TextModelOptions.CompressedStoragePageSize = options.GetOptionValue(TextModelEditorOptions.CompressedStoragePageSizeOptionId); + TextModelOptions.CompressedStorageMaxLoadedPages = options.GetOptionValue(TextModelEditorOptions.CompressedStorageMaxLoadedPagesOptionId); + TextModelOptions.CompressedStorageRetainWeakReferences = options.GetOptionValue(TextModelEditorOptions.CompressedStorageRetainWeakReferencesOptionId); + TextModelOptions.DiffSizeThreshold = options.GetOptionValue(TextModelEditorOptions.DiffSizeThresholdOptionId); + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/BufferPrimitives.cs b/src/Text/Impl/EditorPrimitives/BufferPrimitives.cs new file mode 100644 index 0000000..6c760f6 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/BufferPrimitives.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + + internal sealed class BufferPrimitives : IBufferPrimitives + { + private TextBuffer _textBuffer; + + public BufferPrimitives(ITextBuffer textBuffer, IBufferPrimitivesFactoryService bufferPrimitivesFactory) + { + _textBuffer = bufferPrimitivesFactory.CreateTextBuffer(textBuffer); + } + #region IBufferPrimitives Members + + public TextBuffer Buffer + { + get { return _textBuffer; } + } + + #endregion + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs new file mode 100644 index 0000000..19923d5 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitive.cs @@ -0,0 +1,134 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System; + using System.Collections.Generic; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + + internal sealed class DefaultBufferPrimitive : TextBuffer + { + #region Private members + private ITextBuffer _textBuffer; + private IBufferPrimitivesFactoryService _bufferPrimitivesFactory; + #endregion + + public DefaultBufferPrimitive(ITextBuffer textBuffer, IBufferPrimitivesFactoryService bufferPrimitivesFactory) + { + _textBuffer = textBuffer; + _bufferPrimitivesFactory = bufferPrimitivesFactory; + } + + public override TextPoint GetTextPoint(int position) + { + if ((position < 0) || (position > _textBuffer.CurrentSnapshot.Length)) + { + throw new ArgumentOutOfRangeException("position"); + } + return _bufferPrimitivesFactory.CreateTextPoint(this, position); + } + + public override TextPoint GetTextPoint(int line, int column) + { + if ((line < 0) || (line > _textBuffer.CurrentSnapshot.LineCount)) + { + throw new ArgumentOutOfRangeException("line"); + } + + ITextSnapshotLine snapshotLine = _textBuffer.CurrentSnapshot.GetLineFromLineNumber(line); + + if ((column < 0) || (column > snapshotLine.Length)) + { + throw new ArgumentOutOfRangeException("column"); + } + return _bufferPrimitivesFactory.CreateTextPoint(this, snapshotLine.Start + column); + } + + public override TextRange GetLine(int line) + { + if ((line < 0) || (line > _textBuffer.CurrentSnapshot.LineCount)) + { + throw new ArgumentOutOfRangeException("line"); + } + + ITextSnapshotLine snapshotLine = _textBuffer.CurrentSnapshot.GetLineFromLineNumber(line); + + return GetTextRange(snapshotLine.Extent.Start, snapshotLine.Extent.End); + } + + public override TextRange GetTextRange(TextPoint startPoint, TextPoint endPoint) + { + if (startPoint == null) + { + throw new ArgumentNullException("startPoint"); + } + if (endPoint == null) + { + throw new ArgumentNullException("endPoint"); + } + + if (!object.ReferenceEquals(startPoint.TextBuffer, this)) + { + throw new ArgumentException(Strings.TextPointFromWrongBuffer); + } + + if (!object.ReferenceEquals(endPoint.TextBuffer, this)) + { + throw new ArgumentException(Strings.TextPointFromWrongBuffer); + } + + return _bufferPrimitivesFactory.CreateTextRange(this, startPoint, endPoint); + } + + public override TextRange GetTextRange(int startPosition, int endPosition) + { + if ((startPosition < 0) || (startPosition > _textBuffer.CurrentSnapshot.Length)) + { + throw new ArgumentOutOfRangeException("startPosition"); + } + + if ((endPosition < 0) || (endPosition > _textBuffer.CurrentSnapshot.Length)) + { + throw new ArgumentOutOfRangeException("endPosition"); + } + + TextPoint startPoint = GetTextPoint(startPosition); + TextPoint endPoint = GetTextPoint(endPosition); + + return _bufferPrimitivesFactory.CreateTextRange(this, startPoint, endPoint); + } + + public override ITextBuffer AdvancedTextBuffer + { + get { return _textBuffer; } + } + + public override TextPoint GetStartPoint() + { + return GetTextPoint(0); + } + + public override TextPoint GetEndPoint() + { + return GetTextPoint(_textBuffer.CurrentSnapshot.Length); + } + + public override IEnumerable<TextRange> Lines + { + get + { + foreach (ITextSnapshotLine line in _textBuffer.CurrentSnapshot.Lines) + { + yield return GetTextRange(line.Start, line.End); + } + } + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitivesFactoryService.cs b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitivesFactoryService.cs new file mode 100644 index 0000000..31af8de --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultBufferPrimitivesFactoryService.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System.ComponentModel.Composition; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Operations; + + [Export(typeof(IBufferPrimitivesFactoryService))] + internal sealed class DefaultBufferPrimitivesFactoryService : IBufferPrimitivesFactoryService + { + [Import] + internal IEditorOptionsFactoryService EditorOptionsFactoryService { get; set; } + + [Import] + internal ITextSearchService TextSearchService { get; set; } + + [Import] + internal ITextStructureNavigatorSelectorService TextStructureNavigatorSelectorService { get; set; } + + #region IBufferPrimitivesFactoryService Members + + public TextBuffer CreateTextBuffer(ITextBuffer textBuffer) + { + TextBuffer textBufferPrimitive = null; + if (!textBuffer.Properties.TryGetProperty<TextBuffer>(EditorPrimitiveIds.BufferPrimitiveId, out textBufferPrimitive)) + { + textBufferPrimitive = new DefaultBufferPrimitive(textBuffer, this); + + textBuffer.Properties.AddProperty(EditorPrimitiveIds.BufferPrimitiveId, textBufferPrimitive); + } + + return textBufferPrimitive; + } + + public TextPoint CreateTextPoint(TextBuffer textBuffer, int position) + { + return new DefaultTextPointPrimitive( + textBuffer, + position, + TextSearchService, + EditorOptionsFactoryService.GetOptions(textBuffer.AdvancedTextBuffer), + TextStructureNavigatorSelectorService.GetTextStructureNavigator(textBuffer.AdvancedTextBuffer), + this); + } + + public TextRange CreateTextRange(TextBuffer textBuffer, TextPoint startPoint, TextPoint endPoint) + { + return new DefaultTextRangePrimitive(startPoint, endPoint, EditorOptionsFactoryService); + } + + #endregion + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultCaretPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultCaretPrimitive.cs new file mode 100644 index 0000000..145ebb3 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultCaretPrimitive.cs @@ -0,0 +1,978 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System; + using System.Collections.ObjectModel; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Formatting; + using Microsoft.VisualStudio.Text.Operations; + + using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + + internal sealed class DefaultCaretPrimitive : Caret + { + #region Private members + private TextView _textView; + private IEditorOptions _editorOptions; + #endregion + + internal DefaultCaretPrimitive(TextView textView, IEditorOptions editorOptions) + { + _textView = textView; + _editorOptions = editorOptions; + } + + public override void MoveToNextCharacter(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + TextPoint endPoint = TextView.Selection.GetEndPoint(); + bool selectionIsEmpty = TextView.Selection.IsEmpty; + + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + // If the selection is not empty, VS clears the selection and sets + // the caret at the end of the selection (regardless of it were reversed or not) + if (!extendSelection && !selectionIsEmpty) + { + MoveTo(endPoint.CurrentPosition); + } + else + { + AdvancedCaret.MoveToNextCaretPosition(); + } + + AdvancedCaret.EnsureVisible(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToPreviousCharacter(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + TextPoint startPoint = TextView.Selection.GetStartPoint(); + bool selectionIsEmpty = TextView.Selection.IsEmpty; + + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + if (!extendSelection && !selectionIsEmpty) + { + MoveTo(startPoint.CurrentPosition); + + } + else + { + AdvancedCaret.MoveToPreviousCaretPosition(); + } + + AdvancedCaret.EnsureVisible(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToBeginningOfPreviousLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + this.MoveToBeginningOfPreviousLine(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToBeginningOfNextLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + this.MoveToBeginningOfNextLine(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToBeginningOfPreviousViewLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + this.MoveToBeginningOfPreviousViewLine(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToBeginningOfNextViewLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + this.MoveToBeginningOfNextViewLine(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToPreviousLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + + ITextViewLine caretLine; + if (this.TextView.Selection.IsEmpty || extendSelection) + { + caretLine = this.TextView.AdvancedTextView.Caret.ContainingTextViewLine; + } + else + { + caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(this.TextView.AdvancedTextView.Selection.Start.Position); + } + + if (caretLine.Start != 0) + { + caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(caretLine.Start - 1); + } + + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + this.AdvancedCaret.MoveTo(caretLine); + this.AdvancedCaret.EnsureVisible(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToNextLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + + ITextViewLine caretLine; + if (this.TextView.Selection.IsEmpty || extendSelection) + { + caretLine = this.TextView.AdvancedTextView.Caret.ContainingTextViewLine; + } + else + { + SnapshotPoint end = this.TextView.AdvancedTextView.Selection.End.Position; + caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(end); + + if ((!caretLine.IsFirstTextViewLineForSnapshotLine) && (end.Position == caretLine.Start.Position)) + { + //The end of the selection is at the seam between two word-wrapped lines. In this case, we want + //the line before (since the selection is drawn to the end of that line rather than to the beginning + //of the other). + caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(end - 1); + } + } + + if ((!caretLine.IsLastTextViewLineForSnapshotLine) || (caretLine.LineBreakLength != 0)) //If we are not on the last line of the file + { + caretLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(caretLine.EndIncludingLineBreak); + } + + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + this.AdvancedCaret.MoveTo(caretLine); + this.AdvancedCaret.EnsureVisible(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveTo(int position, bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + SnapshotPoint bufferPosition = new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, position); + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + AdvancedCaret.MoveTo(bufferPosition); + AdvancedCaret.EnsureVisible(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MovePageUp() + { + PageUpDown(ScrollDirection.Up); + } + + public override void MovePageDown() + { + PageUpDown(ScrollDirection.Down); + } + + public override void MovePageUp(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + MovePageUp(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MovePageDown(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + MovePageDown(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToEndOfLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + + DisplayTextPoint caret = this.CaretPoint; + if ((!this.TextView.Selection.IsEmpty) && !extendSelection) + { + caret = this.TextView.GetTextPoint(this.TextView.Selection.AdvancedSelection.End.Position); + } + caret.MoveToEndOfLine(); + + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + this.AdvancedCaret.MoveTo(caret.AdvancedTextPoint, PositionAffinity.Successor); + + this.AdvancedCaret.EnsureVisible(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToStartOfLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + + DisplayTextPoint caret = this.CaretPoint; + if ((!this.TextView.Selection.IsEmpty) && !extendSelection) + { + caret = this.TextView.GetTextPoint(this.TextView.Selection.AdvancedSelection.Start.Position); + } + caret.MoveToStartOfLine(); + + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + this.AdvancedCaret.MoveTo(caret.AdvancedTextPoint, PositionAffinity.Successor); + + this.AdvancedCaret.EnsureVisible(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToEndOfViewLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + SnapshotPoint selectionEnd = this.TextView.Selection.AdvancedSelection.End.Position; + bool selectionIsEmpty = TextView.Selection.IsEmpty; + + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + if (!selectionIsEmpty && !extendSelection) + { + var selectionEndLine = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(selectionEnd); + AdvancedCaret.MoveTo(selectionEndLine.End, PositionAffinity.Predecessor); + } + else + { + ITextViewLine line = this.AdvancedCaret.ContainingTextViewLine; + AdvancedCaret.MoveTo(line.End, line.IsLastTextViewLineForSnapshotLine ? PositionAffinity.Successor : PositionAffinity.Predecessor); + } + + AdvancedCaret.EnsureVisible(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToStartOfViewLine(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + SnapshotPoint selectionStart = this.TextView.Selection.AdvancedSelection.Start.Position; + bool selectionIsEmpty = TextView.Selection.IsEmpty; + bool selectionIsReversed = TextView.Selection.AdvancedSelection.IsReversed; + + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + //We can't use the default behavior of the DisplayTextPoint.MoveToStartOfViewLine since the ITextViewLine + //the caret is on may not be the same as the ITextViewLine the DisplayTextPoint created from the caret thinks + //it is on (c.f. word wrap & affinity). Simulate the behavior of the DisplayTextPoint, adjusting for the + //ITextViewLine and selection. + ITextViewLine line; + if (!selectionIsEmpty && !selectionIsReversed && !extendSelection) + { + line = this.TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(selectionStart); + } + else + { + line = this.AdvancedCaret.ContainingTextViewLine; + } + + SnapshotPoint startOfLine = line.Start; + + this.AdvancedCaret.MoveTo(startOfLine, PositionAffinity.Successor); + this.AdvancedCaret.EnsureVisible(); + this.UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToLine(int lineNumber, bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + MoveToLine(lineNumber); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToLine(int lineNumber, int offset, bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + ITextSnapshotLine line = TextView.AdvancedTextView.TextSnapshot.GetLineFromLineNumber(lineNumber); + AdvancedCaret.MoveTo(new VirtualSnapshotPoint(line, offset)); + + AdvancedCaret.EnsureVisible(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToStartOfDocument(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + AdvancedCaret.MoveTo(new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, 0)); + AdvancedCaret.EnsureVisible(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToEndOfDocument(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + AdvancedCaret.MoveTo(new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, + TextView.AdvancedTextView.TextSnapshot.Length)); + AdvancedCaret.EnsureVisible(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToNextWord(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + DisplayTextPoint caretLocation = CaretPoint; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + caretLocation.MoveToNextWord(); + AdvancedCaret.MoveTo(caretLocation.AdvancedTextPoint); + AdvancedCaret.EnsureVisible(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void MoveToPreviousWord(bool extendSelection) + { + VirtualSnapshotPoint oldCaretPoint = this.AdvancedCaret.Position.VirtualBufferPosition; + DisplayTextPoint caretLocation = CaretPoint; + if (!extendSelection) + { + TextView.AdvancedTextView.Selection.Clear(); + } + + caretLocation.MoveToPreviousWord(); + AdvancedCaret.MoveTo(caretLocation.AdvancedTextPoint); + AdvancedCaret.EnsureVisible(); + UpdateSelection(extendSelection, oldCaretPoint); + } + + public override void EnsureVisible() + { + AdvancedCaret.EnsureVisible(); + } + + public override ITextCaret AdvancedCaret + { + get { return _textView.AdvancedTextView.Caret; } + } + + public override TextView TextView + { + get { return _textView; } + } + + public override ITextViewLine AdvancedTextViewLine + { + get + { + return this.AdvancedCaret.ContainingTextViewLine; + } + } + + public override int DisplayColumn + { + get + { + ITextViewLine textViewLine = AdvancedTextViewLine; + + if ((textViewLine != null) && (textViewLine.IsValid)) + { + return PrimitivesUtilities.GetColumnOfPoint( + TextView.AdvancedTextView.TextSnapshot, + AdvancedTextPoint, + textViewLine.Start, + _editorOptions.GetTabSize(), + (p) => (textViewLine.GetTextElementSpan(p).End)); + } + else + { + return CaretPoint.DisplayColumn; + } + } + } + + public override bool IsVisible + { + get { return CaretPoint.IsVisible; } + } + + public override DisplayTextRange GetDisplayTextRange(DisplayTextPoint otherPoint) + { + return CaretPoint.GetDisplayTextRange(otherPoint); + } + + public override DisplayTextRange GetDisplayTextRange(int otherPosition) + { + return CaretPoint.GetDisplayTextRange(otherPosition); + } + + protected override DisplayTextPoint CloneDisplayTextPointInternal() + { + return CaretPoint.Clone(); + } + + public override TextBuffer TextBuffer + { + get { return _textView.TextBuffer; } + } + + public override int CurrentPosition + { + get { return CaretPoint.CurrentPosition; } + } + + public override int Column + { + get { return CaretPoint.Column; } + } + + public override bool DeleteNext() + { + if (TextView.Selection.IsEmpty) + { + if (!CaretPoint.DeleteNext()) + return false; + } + else + { + if (!TextView.Selection.Delete()) + return false; + } + + AdvancedCaret.EnsureVisible(); + + return true; + } + + public override bool DeletePrevious() + { + if (TextView.Selection.IsEmpty) + { + if (AdvancedCaret.InVirtualSpace) + { + AdvancedCaret.MoveToPreviousCaretPosition(); + } + else + { + if (!CaretPoint.DeletePrevious()) + return false; + if (AdvancedCaret.InVirtualSpace) + AdvancedCaret.MoveTo(AdvancedCaret.Position.BufferPosition); + } + } + else + { + if (!TextView.Selection.Delete()) + return false; + } + + AdvancedCaret.EnsureVisible(); + + return true; + } + + public override TextRange GetCurrentWord() + { + return CaretPoint.GetCurrentWord(); + } + + public override TextRange GetNextWord() + { + return CaretPoint.GetNextWord(); + } + + public override TextRange GetPreviousWord() + { + return CaretPoint.GetPreviousWord(); + } + + public override TextRange GetTextRange(TextPoint otherPoint) + { + return CaretPoint.GetTextRange(otherPoint); + } + + public override TextRange GetTextRange(int otherPosition) + { + return CaretPoint.GetTextRange(otherPosition); + } + + public override bool InsertNewLine() + { + if (!TextView.Selection.IsEmpty) + { + if (!TextView.Selection.Delete()) + return false; + } + + if (!CaretPoint.InsertNewLine()) + return false; + + AdvancedCaret.EnsureVisible(); + + return true; + } + + public override bool InsertIndent() + { + if (!CaretPoint.InsertIndent()) + return false; + + AdvancedCaret.EnsureVisible(); + + return true; + } + + public override bool InsertText(string text) + { + if (!TextView.Selection.IsEmpty) + { + if (!TextView.Selection.Delete()) + return false; + } + + if (!CaretPoint.InsertText(text)) + return false; + + AdvancedCaret.EnsureVisible(); + + return true; + } + + public override int LineNumber + { + get { return CaretPoint.LineNumber; } + } + + public override int StartOfLine + { + get { return CaretPoint.StartOfLine; } + } + + public override int EndOfLine + { + get { return CaretPoint.EndOfLine; } + } + + public override int StartOfViewLine + { + get { return this.AdvancedTextViewLine.Start; } + } + + public override int EndOfViewLine + { + get { return this.AdvancedTextViewLine.End; } + } + + public override bool RemovePreviousIndent() + { + if (!CaretPoint.RemovePreviousIndent()) + return false; + + AdvancedCaret.EnsureVisible(); + + return true; + } + + public override bool TransposeCharacter() + { + if (!CaretPoint.TransposeCharacter()) + return false; + + TextView.Selection.Clear(); + AdvancedCaret.EnsureVisible(); + + return true; + } + + public override bool TransposeLine() + { + if (TextView.Selection.IsEmpty) + { + TextPoint caretPoint = CaretPoint; + double oldLeft = AdvancedCaret.Left; + + if (!caretPoint.TransposeLine()) + return false; + + AdvancedCaret.MoveTo(caretPoint.AdvancedTextPoint, PositionAffinity.Successor, false); + AdvancedCaret.EnsureVisible(); + + ITextViewLine newLine = AdvancedTextViewLine; + AdvancedCaret.MoveTo(newLine, oldLeft); + AdvancedCaret.EnsureVisible(); + } + return true; + } + + public override bool TransposeLine(int lineNumber) + { + if (!TextView.Selection.IsEmpty) + return true; + + if (!CaretPoint.TransposeLine(lineNumber)) + return false; + + AdvancedCaret.EnsureVisible(); + + return true; + } + + public override SnapshotPoint AdvancedTextPoint + { + get { return CaretPoint.AdvancedTextPoint; } + } + + public override string GetNextCharacter() + { + return CaretPoint.GetNextCharacter(); + } + + public override string GetPreviousCharacter() + { + return CaretPoint.GetPreviousCharacter(); + } + + public override TextRange Find(string pattern, FindOptions findOptions, TextPoint endPoint) + { + return CaretPoint.Find(pattern, findOptions, endPoint); + } + + public override TextRange Find(string pattern, TextPoint endPoint) + { + return CaretPoint.Find(pattern, endPoint); + } + + public override TextRange Find(string pattern, FindOptions findOptions) + { + return CaretPoint.Find(pattern, findOptions); + } + + public override TextRange Find(string pattern) + { + return CaretPoint.Find(pattern); + } + + public override Collection<TextRange> FindAll(string pattern, TextPoint endPoint) + { + return CaretPoint.FindAll(pattern, endPoint); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions, TextPoint endPoint) + { + return CaretPoint.FindAll(pattern, findOptions, endPoint); + } + + public override Collection<TextRange> FindAll(string pattern) + { + return CaretPoint.FindAll(pattern); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions) + { + return CaretPoint.FindAll(pattern, findOptions); + } + + public override void MoveTo(int position) + { + MoveTo(position, false); + } + + public override void MoveToNextCharacter() + { + MoveToNextCharacter(false); + } + + public override void MoveToPreviousCharacter() + { + MoveToPreviousCharacter(false); + } + + public override void MoveToLine(int lineNumber) + { + DisplayTextPoint caretLocation = CaretPoint; + TextView.Selection.Clear(); + caretLocation.MoveToLine(lineNumber); + AdvancedCaret.MoveTo(caretLocation.AdvancedTextPoint); + AdvancedCaret.EnsureVisible(); + } + + public override void MoveToEndOfLine() + { + MoveToEndOfLine(false); + } + + public override void MoveToStartOfLine() + { + MoveToStartOfLine(false); + } + + public override void MoveToEndOfViewLine() + { + MoveToEndOfViewLine(false); + } + + public override void MoveToStartOfViewLine() + { + MoveToStartOfViewLine(false); + } + + public override void MoveToEndOfDocument() + { + MoveToEndOfDocument(false); + } + + public override void MoveToStartOfDocument() + { + MoveToStartOfDocument(false); + } + + public override void MoveToBeginningOfNextLine() + { + DisplayTextPoint caret = this.CaretPoint; + caret.MoveToBeginningOfNextLine(); + this.AdvancedCaret.MoveTo(caret.AdvancedTextPoint); + this.AdvancedCaret.EnsureVisible(); + } + + public override void MoveToBeginningOfPreviousLine() + { + DisplayTextPoint caret = this.CaretPoint; + caret.MoveToBeginningOfPreviousLine(); + this.AdvancedCaret.MoveTo(caret.AdvancedTextPoint); + this.AdvancedCaret.EnsureVisible(); + } + + public override void MoveToBeginningOfNextViewLine() + { + this.AdvancedCaret.MoveTo(this.AdvancedTextViewLine.EndIncludingLineBreak); + this.AdvancedCaret.EnsureVisible(); + } + + public override void MoveToBeginningOfPreviousViewLine() + { + ITextViewLine line = this.AdvancedTextViewLine; + if (line.Start > 0) + line = _textView.AdvancedTextView.GetTextViewLineContainingBufferPosition(line.Start - 1); + + this.AdvancedCaret.MoveTo(line.Start); + this.AdvancedCaret.EnsureVisible(); + } + + public override void MoveToNextWord() + { + MoveToNextWord(false); + } + + public override void MoveToPreviousWord() + { + MoveToPreviousWord(false); + } + + // Note: This overrides the equivalent DisplayTextPoint method in order to use the caret's + // ContainingTextViewLine, which can differ at the wrap points of word-wrapped lines. + public override DisplayTextPoint GetFirstNonWhiteSpaceCharacterOnViewLine() + { + ITextViewLine viewLine = this.AdvancedTextViewLine; + ITextSnapshot snapshot = viewLine.Extent.Snapshot; + + int firstNonWhitespaceCharacter = viewLine.Start; + while ((firstNonWhitespaceCharacter < viewLine.End) && + char.IsWhiteSpace(snapshot[firstNonWhitespaceCharacter])) + { + firstNonWhitespaceCharacter++; + } + + return TextView.GetTextPoint(firstNonWhitespaceCharacter); + } + + public override TextPoint GetFirstNonWhiteSpaceCharacterOnLine() + { + return CaretPoint.GetFirstNonWhiteSpaceCharacterOnLine(); + } + + #region Private methods + private DisplayTextPoint CaretPoint + { + get + { + return TextView.GetTextPoint(_textView.AdvancedTextView.Caret.Position.BufferPosition); + } + } + + /// <summary> + /// Update the selection based on the movement of the caret. + /// </summary> + private void UpdateSelection(bool extendSelection, VirtualSnapshotPoint oldPosition) + { + if (extendSelection) + { + if (TextView.Selection.IsEmpty) + { + //The provided old position might be stale + this.TextView.AdvancedTextView.Selection.Select(oldPosition.TranslateTo(this.TextView.AdvancedTextView.TextSnapshot), + this.AdvancedCaret.Position.VirtualBufferPosition); + } + else + { + this.TextView.AdvancedTextView.Selection.Select(this.TextView.AdvancedTextView.Selection.AnchorPoint, + this.AdvancedCaret.Position.VirtualBufferPosition); + } + } + else + { + TextView.AdvancedTextView.Selection.Clear(); + } + } + + private void PageUpDown(ScrollDirection direction) + { + if (direction == ScrollDirection.Up) + { + //If we scrolled a full page, then the bottom of the first fully visible line will be below + //the bottom of the view. + ITextViewLine firstVisibleLine = TextView.AdvancedTextView.TextViewLines.FirstVisibleLine; + SnapshotPoint oldFullyVisibleStart = (firstVisibleLine.VisibilityState == VisibilityState.FullyVisible) + ? firstVisibleLine.Start + : firstVisibleLine.EndIncludingLineBreak; //Start of next line. + + if (TextView.AdvancedTextView.ViewScroller.ScrollViewportVerticallyByPage(direction)) + { + ITextViewLine newFirstLine = TextView.AdvancedTextView.TextViewLines.GetTextViewLineContainingBufferPosition(oldFullyVisibleStart); + + //The old fully visible line should -- if we scrolled as much as we could -- be partially + //obscured. The shortfall between a full page and what we actually scrolled is the distance + //between the bottom of that line and the bottom of the screen. + if (TextView.AdvancedTextView.ViewportBottom > newFirstLine.Bottom) + { + AdvancedCaret.MoveTo(TextView.AdvancedTextView.TextViewLines.FirstVisibleLine); + } + else + { + AdvancedCaret.MoveToPreferredCoordinates(); + } + } + } + else + { + //If we scroll a full page, the bottom of the last fully visible line will be + //positioned at 0.0. + ITextViewLine lastVisibleLine = TextView.AdvancedTextView.TextViewLines.LastVisibleLine; + + // If the last line in the buffer is fully visible , then just move the caret to + // the last visible line + if ((lastVisibleLine.VisibilityState == VisibilityState.FullyVisible) && + (lastVisibleLine.End == lastVisibleLine.Snapshot.Length)) + { + AdvancedCaret.MoveTo(lastVisibleLine); + + // No need to ensure the caret is visible since the line it just moved to is fully visible. + return; + } + + SnapshotPoint oldFullyVisibleStart = ((lastVisibleLine.VisibilityState == VisibilityState.FullyVisible) || (lastVisibleLine.Start == 0)) + ? lastVisibleLine.Start + : (lastVisibleLine.Start - 1); //Actually just a point on the previous line. + + if (TextView.AdvancedTextView.ViewScroller.ScrollViewportVerticallyByPage(direction)) + { + ITextViewLine newLastLine = TextView.AdvancedTextView.TextViewLines.GetTextViewLineContainingBufferPosition(oldFullyVisibleStart); + if (newLastLine.Bottom > TextView.AdvancedTextView.ViewportTop) + { + AdvancedCaret.MoveTo(TextView.AdvancedTextView.TextViewLines.LastVisibleLine); + } + else + { + AdvancedCaret.MoveToPreferredCoordinates(); + } + } + } + + EnsureVisible(); + } + #endregion + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs new file mode 100644 index 0000000..0b64bc4 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextPointPrimitive.cs @@ -0,0 +1,499 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System; + using System.Collections.ObjectModel; + using System.Globalization; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Formatting; + using Microsoft.VisualStudio.Text.Operations; + + using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + using System.Diagnostics; + + internal sealed class DefaultDisplayTextPointPrimitive : DisplayTextPoint + { + private TextView _textView; + private TextPoint _bufferPoint; + private IEditorOptions _editorOptions; + + internal DefaultDisplayTextPointPrimitive(TextView textView, int position, IEditorOptions editorOptions) + { + _textView = textView; + _editorOptions = editorOptions; + + _bufferPoint = _textView.TextBuffer.GetStartPoint(); + + // The position coming in will be from the view's snapshot which may not be the same snapshot as the buffer. + // Force the position to track to the buffer's snapshot to prevent the position from being valid in the view's + // snapshot but invalid in the buffers + ITrackingPoint trackingPoint = _textView.AdvancedTextView.TextSnapshot.CreateTrackingPoint(position, PointTrackingMode.Positive); + + MoveTo(trackingPoint.GetPosition(_textView.TextBuffer.AdvancedTextBuffer.CurrentSnapshot)); + } + + public override TextView TextView + { + get { return _textView; } + } + + public override ITextViewLine AdvancedTextViewLine + { + get + { + return _textView.AdvancedTextView.GetTextViewLineContainingBufferPosition(this.AdvancedTextPoint); + } + } + + public override int DisplayColumn + { + get + { + ITextView textView = _textView.AdvancedTextView; + SnapshotPoint position = this.AdvancedTextViewLine.Start; + + return PrimitivesUtilities.GetColumnOfPoint( + textView.TextSnapshot, + AdvancedTextPoint, + position, + _editorOptions.GetTabSize(), + (p) => (textView.GetTextElementSpan(p).End)); + } + } + + public override bool IsVisible + { + get + { + // if no lines are being drawn then this point can't be visible + return (this.AdvancedTextViewLine.VisibilityState == VisibilityState.FullyVisible); + } + } + + public override DisplayTextRange GetDisplayTextRange(DisplayTextPoint otherPoint) + { + if (!object.ReferenceEquals(this.TextBuffer, otherPoint.TextBuffer)) + { + throw new ArgumentException("The other point must have the same TextBuffer as this one", "otherPoint"); + } + return TextView.GetTextRange(this, otherPoint); + } + + public override DisplayTextRange GetDisplayTextRange(int otherPosition) + { + return TextView.GetTextRange(CurrentPosition, otherPosition); + } + + protected override DisplayTextPoint CloneDisplayTextPointInternal() + { + return new DefaultDisplayTextPointPrimitive(_textView, CurrentPosition, _editorOptions); + } + + public override TextBuffer TextBuffer + { + get { return _textView.TextBuffer; } + } + + public override int CurrentPosition + { + get { return _bufferPoint.CurrentPosition; } + } + + public override int Column + { + get { return _bufferPoint.Column; } + } + + public override bool DeleteNext() + { + if (_textView.AdvancedTextView.TextViewModel.IsPointInVisualBuffer(AdvancedTextPoint, PositionAffinity.Successor)) + { + return PrimitivesUtilities.Delete(GetNextTextElementSpan()); + } + else + { + return _bufferPoint.DeleteNext(); + } + } + + public override bool DeletePrevious() + { + SnapshotSpan previousElementSpan = GetPreviousTextElementSpan(); + + if ((previousElementSpan.Length > 0) && + (_textView.AdvancedTextView.TextViewModel.IsPointInVisualBuffer(AdvancedTextPoint, PositionAffinity.Successor)) && + (!_textView.AdvancedTextView.TextViewModel.IsPointInVisualBuffer(previousElementSpan.End - 1, PositionAffinity.Successor))) + { + // Since the previous character is not visible but the current one is, delete + // the entire previous text element span. + return PrimitivesUtilities.Delete(previousElementSpan); + } + else + { + // Delegate to the buffer point's DeletePrevious implementation to handle deleting single + // characters. A single character should be deleted if this point and the previous one + // are both visible or both not visible. + return _bufferPoint.DeletePrevious(); + } + } + + public override TextRange GetCurrentWord() + { + return _bufferPoint.GetCurrentWord(); + } + + public override TextRange GetNextWord() + { + return _bufferPoint.GetNextWord(); + } + + public override TextRange GetPreviousWord() + { + return _bufferPoint.GetPreviousWord(); + } + + public override TextRange GetTextRange(TextPoint otherPoint) + { + return _bufferPoint.GetTextRange(otherPoint); + } + + public override TextRange GetTextRange(int otherPosition) + { + return _bufferPoint.GetTextRange(otherPosition); + } + + public override bool InsertNewLine() + { + return _bufferPoint.InsertNewLine(); + } + + public override bool InsertIndent() + { + return _bufferPoint.InsertIndent(); + } + + public override bool InsertText(string text) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + + return _bufferPoint.InsertText(text); + } + + public override int LineNumber + { + get { return _bufferPoint.LineNumber; } + } + + public override int StartOfLine + { + get { return _bufferPoint.StartOfLine; } + } + + public override int EndOfLine + { + get { return _bufferPoint.EndOfLine; } + } + + public override int StartOfViewLine + { + get + { + return this.AdvancedTextViewLine.Start; + } + } + + public override int EndOfViewLine + { + get + { + return this.AdvancedTextViewLine.End; + } + } + + public override bool RemovePreviousIndent() + { + return _bufferPoint.RemovePreviousIndent(); + } + + public override bool TransposeCharacter() + { + SnapshotPoint insertionPoint = AdvancedTextPoint; + + ITextSnapshotLine line = _textView.AdvancedTextView.TextSnapshot.GetLineFromPosition(insertionPoint); + + string lineText = line.GetText(); + + if (StringInfo.ParseCombiningCharacters(lineText).Length < 2) + { + return true; + } + + SnapshotSpan textElementLeftSpan, textElementRightSpan; + + // We're at the start of a line + if (insertionPoint == line.Start) + { + textElementLeftSpan = TextView.AdvancedTextView.GetTextElementSpan(insertionPoint); + textElementRightSpan = TextView.AdvancedTextView.GetTextElementSpan(textElementLeftSpan.End); + } + // We're at the end of a line + else if (insertionPoint == line.End) + { + textElementRightSpan = TextView.AdvancedTextView.GetTextElementSpan(insertionPoint - 1); + textElementLeftSpan = TextView.AdvancedTextView.GetTextElementSpan(textElementRightSpan.Start - 1); + } + // We're at the middle of a line + else + { + textElementRightSpan = TextView.AdvancedTextView.GetTextElementSpan(insertionPoint); + textElementLeftSpan = TextView.AdvancedTextView.GetTextElementSpan(textElementRightSpan.Start - 1); + } + + string transposedText = _textView.AdvancedTextView.TextSnapshot.GetText(textElementRightSpan) + + _textView.AdvancedTextView.TextSnapshot.GetText(textElementLeftSpan); + + return PrimitivesUtilities.Replace(TextBuffer.AdvancedTextBuffer, new Span(textElementLeftSpan.Start, transposedText.Length), transposedText); + } + + public override bool TransposeLine() + { + return _bufferPoint.TransposeLine(); + } + + public override bool TransposeLine(int lineNumber) + { + return _bufferPoint.TransposeLine(lineNumber); + } + + public override SnapshotPoint AdvancedTextPoint + { + // TODO!: Should this not translate (and throw instead)? Should it be positive or negative? + // Use positive tracking to behave like the caret would. + get + { + Debug.Assert(_bufferPoint.AdvancedTextPoint.Snapshot == TextView.AdvancedTextView.TextSnapshot, + "WARNING: We are tracking a SnapshotPoint in a display primitive, which could have bad consequences."); + + return _bufferPoint.AdvancedTextPoint.TranslateTo(TextView.AdvancedTextView.TextSnapshot, + PointTrackingMode.Positive); + } + } + + public override string GetNextCharacter() + { + if (CurrentPosition == _textView.AdvancedTextView.TextSnapshot.Length) + { + return string.Empty; + } + return _textView.AdvancedTextView.TextSnapshot.GetText(TextView.AdvancedTextView.GetTextElementSpan(AdvancedTextPoint)); + } + + public override string GetPreviousCharacter() + { + if (CurrentPosition == 0) + { + return string.Empty; + } + return _textView.AdvancedTextView.TextSnapshot.GetText(GetPreviousTextElementSpan()); + } + + public override TextRange Find(string pattern, FindOptions findOptions, TextPoint endPoint) + { + return _bufferPoint.Find(pattern, findOptions, endPoint); + } + + public override TextRange Find(string pattern, TextPoint endPoint) + { + return _bufferPoint.Find(pattern, endPoint); + } + + public override TextRange Find(string pattern, FindOptions findOptions) + { + return _bufferPoint.Find(pattern, findOptions); + } + + public override TextRange Find(string pattern) + { + return _bufferPoint.Find(pattern); + } + + public override Collection<TextRange> FindAll(string pattern, TextPoint endPoint) + { + return _bufferPoint.FindAll(pattern, endPoint); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions, TextPoint endPoint) + { + return _bufferPoint.FindAll(pattern, findOptions, endPoint); + } + + public override Collection<TextRange> FindAll(string pattern) + { + return _bufferPoint.FindAll(pattern); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions) + { + return _bufferPoint.FindAll(pattern, findOptions); + } + + public override void MoveTo(int position) + { + SnapshotPoint point = new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, position); + + _bufferPoint.MoveTo(TextView.AdvancedTextView.GetTextElementSpan(point).Start); + } + + public override void MoveToNextCharacter() + { + int currentPosition = this.CurrentPosition; + + MoveTo(GetNextTextElementSpan().End); + + // It's possible that the above code didn't actually move this point. Dev11 #109752 shows how + // this can happen, which is that GetNextTextElementSpan() above (which calls into WPF) may say + // that the current character doesn't combine with anything, but the _bufferPoint (which calls into + // framework methods that follow the unicode standard) will say that the character does combine. In that case + // the GetNextTextElementSpan().End isn't far enough away for this point to escape the combining character + // sequence, and the point is left where it is, at the start of a base character. + if (currentPosition == this.CurrentPosition && + currentPosition != this.AdvancedTextPoint.Snapshot.Length) + { + _bufferPoint.MoveToNextCharacter(); + + Debug.Assert(currentPosition != this.CurrentPosition, + "DisplayTextPoint.MoveToNextCharacter was unable to successfully move forward. This may cause unexpected behavior or hangs."); + } + } + + public override void MoveToPreviousCharacter() + { + MoveTo(GetPreviousTextElementSpan().Start); + } + + public override void MoveToLine(int lineNumber) + { + _bufferPoint.MoveToLine(lineNumber); + } + + public override void MoveToEndOfLine() + { + _bufferPoint.MoveToEndOfLine(); + } + + public override void MoveToStartOfLine() + { + _bufferPoint.MoveToStartOfLine(); + } + + public override void MoveToEndOfViewLine() + { + MoveTo(this.EndOfViewLine); + } + + public override void MoveToStartOfViewLine() + { + MoveTo(this.StartOfViewLine); + } + + public override void MoveToEndOfDocument() + { + _bufferPoint.MoveToEndOfDocument(); + } + + public override void MoveToStartOfDocument() + { + _bufferPoint.MoveToStartOfDocument(); + } + + public override void MoveToBeginningOfNextLine() + { + _bufferPoint.MoveToBeginningOfNextLine(); + } + + public override void MoveToBeginningOfPreviousLine() + { + _bufferPoint.MoveToBeginningOfPreviousLine(); + } + + public override void MoveToBeginningOfNextViewLine() + { + this.MoveTo(this.AdvancedTextViewLine.EndIncludingLineBreak); + } + + public override void MoveToBeginningOfPreviousViewLine() + { + ITextViewLine line = this.AdvancedTextViewLine; + if (line.Start > 0) + line = _textView.AdvancedTextView.GetTextViewLineContainingBufferPosition(line.Start - 1); + + this.MoveTo(line.Start); + } + + public override void MoveToNextWord() + { + _bufferPoint.MoveToNextWord(); + + // make sure the word structure navigator didn't return a position in the middle of a view element + SnapshotPoint bufferPoint = _bufferPoint.AdvancedTextPoint; + SnapshotSpan textElementSpan = _textView.AdvancedTextView.GetTextElementSpan(bufferPoint); + if (bufferPoint > textElementSpan.Start) + this.MoveTo(textElementSpan.End); + } + + public override void MoveToPreviousWord() + { + _bufferPoint.MoveToPreviousWord(); + + // make sure the word structure navigator didn't return a position in the middle of a view element + this.MoveTo(_bufferPoint.AdvancedTextPoint); + } + + public override DisplayTextPoint GetFirstNonWhiteSpaceCharacterOnViewLine() + { + ITextViewLine viewLine = _textView.AdvancedTextView.GetTextViewLineContainingBufferPosition(AdvancedTextPoint); + ITextSnapshot snapshot = viewLine.Extent.Snapshot; + + int firstNonWhitespaceCharacter = viewLine.Start; + while ((firstNonWhitespaceCharacter < viewLine.End) && + char.IsWhiteSpace(snapshot[firstNonWhitespaceCharacter])) + { + firstNonWhitespaceCharacter++; + } + + return new DefaultDisplayTextPointPrimitive(_textView, firstNonWhitespaceCharacter, _editorOptions); + } + + public override TextPoint GetFirstNonWhiteSpaceCharacterOnLine() + { + return _bufferPoint.GetFirstNonWhiteSpaceCharacterOnLine(); + } + + #region Private helpers + private SnapshotSpan GetNextTextElementSpan() + { + return TextView.AdvancedTextView.GetTextElementSpan(AdvancedTextPoint); + } + + private SnapshotSpan GetPreviousTextElementSpan() + { + if (CurrentPosition == 0) + { + return new SnapshotSpan(AdvancedTextPoint, 0); + } + return TextView.AdvancedTextView.GetTextElementSpan(AdvancedTextPoint - 1); + } + #endregion + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultDisplayTextRangePrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextRangePrimitive.cs new file mode 100644 index 0000000..784892d --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultDisplayTextRangePrimitive.cs @@ -0,0 +1,225 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System.Collections.Generic; + using System.Collections.ObjectModel; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Formatting; + using Microsoft.VisualStudio.Text.Operations; + + internal sealed class DefaultDisplayTextRangePrimitive : DisplayTextRange + { + private TextView _textView; + private TextRange _bufferRange; + + internal DefaultDisplayTextRangePrimitive(TextView textView, TextRange bufferRange) + { + _textView = textView; + _bufferRange = bufferRange.Clone(); + + MoveTo(bufferRange); + } + + public override TextView TextView + { + get { return _textView; } + } + + public override DisplayTextPoint GetDisplayStartPoint() + { + return TextView.GetTextPoint(_bufferRange.GetStartPoint()); + } + + public override DisplayTextPoint GetDisplayEndPoint() + { + return TextView.GetTextPoint(_bufferRange.GetEndPoint()); + } + + public override VisibilityState Visibility + { + get + { + ITextViewLineCollection renderedTextLines = TextView.AdvancedTextView.TextViewLines; + + SnapshotSpan range = new SnapshotSpan(GetStartPoint().AdvancedTextPoint, + GetEndPoint().AdvancedTextPoint); + SnapshotSpan? overlap = renderedTextLines.FormattedSpan.Overlap(range); + + if (overlap.HasValue) + { + ITextViewLine startLine = renderedTextLines.GetTextViewLineContainingBufferPosition(overlap.Value.Start); + ITextViewLine endLine = renderedTextLines.GetTextViewLineContainingBufferPosition(overlap.Value.End); + + VisibilityState startVisibility = startLine.VisibilityState; + + if (startLine == endLine) + { + //Only a single line: visibility is whatever that line is. + return startVisibility; + } + + VisibilityState endVisibility = endLine.VisibilityState; + + if ((startVisibility == VisibilityState.FullyVisible) && + (endVisibility == VisibilityState.FullyVisible)) + { + //Both lines are fully visible and so is everything in between them. + return VisibilityState.FullyVisible; + } + + if ((startVisibility != VisibilityState.Hidden) || + (endVisibility != VisibilityState.Hidden) || + (startLine.EndIncludingLineBreak != endLine.Start)) + { + //Either one of the lines is not hidden (so the range is partially visible) or + //there are lines between the start that can't be hidden. + return VisibilityState.PartiallyVisible; + } + } + + return VisibilityState.Hidden; + } + } + + public override bool IsEmpty + { + get { return _bufferRange.IsEmpty; } + } + + protected override DisplayTextRange CloneDisplayTextRangeInternal() + { + return new DefaultDisplayTextRangePrimitive(_textView, _bufferRange); + } + + protected override IEnumerator<DisplayTextPoint> GetDisplayPointEnumeratorInternal() + { + DisplayTextPoint displayTextPoint = GetDisplayStartPoint(); + DisplayTextPoint endPoint = GetDisplayEndPoint(); + while (displayTextPoint.CurrentPosition <= endPoint.CurrentPosition) + { + yield return displayTextPoint; + + if (displayTextPoint.CurrentPosition == displayTextPoint.AdvancedTextPoint.Snapshot.Length) + { + break; + } + displayTextPoint = displayTextPoint.Clone(); + displayTextPoint.MoveToNextCharacter(); + } + } + + public override TextPoint GetStartPoint() + { + return _bufferRange.GetStartPoint(); + } + + public override TextPoint GetEndPoint() + { + return _bufferRange.GetEndPoint(); + } + + public override TextBuffer TextBuffer + { + get { return _textView.TextBuffer; } + } + + public override SnapshotSpan AdvancedTextRange + { + get { return _bufferRange.AdvancedTextRange; } + } + + public override bool MakeUppercase() + { + return _bufferRange.MakeUppercase(); + } + + public override bool MakeLowercase() + { + return _bufferRange.MakeLowercase(); + } + + public override bool Capitalize() + { + return _bufferRange.Capitalize(); + } + + public override bool ToggleCase() + { + return _bufferRange.ToggleCase(); + } + + public override bool Delete() + { + return _bufferRange.Delete(); + } + + public override bool Indent() + { + return _bufferRange.Indent(); + } + + public override bool Unindent() + { + return _bufferRange.Unindent(); + } + + public override TextRange Find(string pattern) + { + return _bufferRange.Find(pattern); + } + + public override TextRange Find(string pattern, FindOptions findOptions) + { + return _bufferRange.Find(pattern, findOptions); + } + + public override Collection<TextRange> FindAll(string pattern) + { + return _bufferRange.FindAll(pattern); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions) + { + return _bufferRange.FindAll(pattern, findOptions); + } + + public override bool ReplaceText(string newText) + { + return _bufferRange.ReplaceText(newText); + } + + public override string GetText() + { + return _bufferRange.GetText(); + } + + public override void SetStart(TextPoint startPoint) + { + _bufferRange.SetStart(TextView.GetTextPoint(startPoint)); + } + + public override void SetEnd(TextPoint endPoint) + { + _bufferRange.SetEnd(TextView.GetTextPoint(endPoint)); + } + + public override void MoveTo(TextRange newRange) + { + SetStart(newRange.GetStartPoint()); + SetEnd(newRange.GetEndPoint()); + } + + protected override IEnumerator<TextPoint> GetEnumeratorInternal() + { + return _bufferRange.GetEnumerator(); + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs new file mode 100644 index 0000000..35231be --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultSelectionPrimitive.cs @@ -0,0 +1,496 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Formatting; + using Microsoft.VisualStudio.Text.Operations; + + using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + + internal sealed class DefaultSelectionPrimitive : Selection + { + private TextView _textView; + private IEditorOptions _editorOptions; + + public DefaultSelectionPrimitive(TextView textView, IEditorOptions editorOptions) + { + _textView = textView; + _editorOptions = editorOptions; + } + + private ITextSelection TextSelection + { + get { return _textView.AdvancedTextView.Selection; } + } + + private ITextCaret Caret + { + get { return TextView.AdvancedTextView.Caret; } + } + + private DisplayTextRange TextRange + { + get + { + // TODO: What do we do in box mode? + return TextView.GetTextRange(TextSelection.Start.Position, TextSelection.End.Position); + } + } + + public override void SelectRange(TextRange textRange) + { + this.SelectRange(textRange.GetStartPoint().CurrentPosition, textRange.GetEndPoint().CurrentPosition); + } + + public override void SelectRange(TextPoint selectionStart, TextPoint selectionEnd) + { + this.SelectRange(selectionStart.CurrentPosition, selectionEnd.CurrentPosition); + } + + public override void SelectAll() + { + // For select all, the selection always goes back to stream mode. + this.AdvancedSelection.Mode = TextSelectionMode.Stream; + this.SelectRange(0, TextView.AdvancedTextView.TextSnapshot.Length); + } + + public override void ExtendSelection(TextPoint newEnd) + { + int selectionStart, selectionEnd = newEnd.CurrentPosition; + + // Now, figure out where the selection actually started + if (IsEmpty) + selectionStart = TextSelection.Start.Position; + else + { + if (IsReversed) + { + selectionStart = GetEndPoint().CurrentPosition; + } + else + { + selectionStart = GetStartPoint().CurrentPosition; + } + } + + this.SelectRange(selectionStart, selectionEnd); + } + + private void SelectRange(int selectionStart, int selectionEnd) + { + SnapshotPoint startPoint = new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, selectionStart); + SnapshotPoint endPoint = new SnapshotPoint(TextView.AdvancedTextView.TextSnapshot, selectionEnd); + + TextSelection.Select(new VirtualSnapshotPoint(startPoint), new VirtualSnapshotPoint(endPoint)); + + ITextViewLine textViewLine = TextView.AdvancedTextView.GetTextViewLineContainingBufferPosition(endPoint); + PositionAffinity affinity = (textViewLine.IsLastTextViewLineForSnapshotLine || (endPoint != textViewLine.End)) ? PositionAffinity.Successor : PositionAffinity.Predecessor; + + Caret.MoveTo(endPoint, affinity); + TextView.AdvancedTextView.ViewScroller.EnsureSpanVisible(TextSelection.StreamSelectionSpan.SnapshotSpan, + (selectionStart <= selectionEnd) + ? EnsureSpanVisibleOptions.MinimumScroll + : (EnsureSpanVisibleOptions.MinimumScroll | EnsureSpanVisibleOptions.ShowStart)); + } + + public override void Clear() + { + TextSelection.Clear(); + } + + public override ITextSelection AdvancedSelection + { + get { return TextSelection; } + } + + public override bool IsEmpty + { + get { return TextSelection.IsEmpty; } + } + + public override bool IsReversed + { + get { return TextSelection.IsReversed; } + set + { + if (value) + { + SelectRange(TextRange.GetEndPoint().CurrentPosition, TextRange.GetStartPoint().CurrentPosition); + } + else + { + SelectRange(TextRange.GetStartPoint().CurrentPosition, TextRange.GetEndPoint().CurrentPosition); + } + } + } + + public override TextView TextView + { + get { return _textView; } + } + + public override DisplayTextPoint GetDisplayStartPoint() + { + return TextRange.GetDisplayStartPoint(); + } + + public override DisplayTextPoint GetDisplayEndPoint() + { + return TextRange.GetDisplayEndPoint(); + } + + public override VisibilityState Visibility + { + get { return TextRange.Visibility; } + } + + protected override DisplayTextRange CloneDisplayTextRangeInternal() + { + return TextRange.Clone(); + } + + protected override IEnumerator<DisplayTextPoint> GetDisplayPointEnumeratorInternal() + { + return TextRange.GetEnumerator(); + } + + public override TextPoint GetStartPoint() + { + return TextRange.GetStartPoint(); + } + + public override TextPoint GetEndPoint() + { + return TextRange.GetEndPoint(); + } + + public override TextBuffer TextBuffer + { + get { return TextView.TextBuffer; } + } + + public override SnapshotSpan AdvancedTextRange + { + get { return TextRange.AdvancedTextRange; } + } + + public override bool MakeUppercase() + { + // NOTE: We store this as a *Span* because we don't want it to track + Span selectionSpan = TextRange.AdvancedTextRange; + bool isReversed = IsReversed; + if (!TextRange.MakeUppercase()) + return false; + TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, selectionSpan), isReversed); + return true; + } + + public override bool MakeLowercase() + { + // NOTE: We store this as a *Span* because we don't want it to track + Span selectionSpan = TextRange.AdvancedTextRange; + bool isReversed = IsReversed; + if (!TextRange.MakeLowercase()) + return false; + TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, selectionSpan), isReversed); + return true; + } + + public override bool Capitalize() + { + // NOTE: We store this as a *Span* because we don't want it to track + Span selectionSpan = TextRange.AdvancedTextRange; + bool isReversed = IsReversed; + bool isEmpty = IsEmpty; + if (!TextRange.Capitalize()) + return false; + + if (!isEmpty) + { + TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, selectionSpan), isReversed); + } + + return true; + } + + public override bool ToggleCase() + { + // NOTE: We store this as a *Span* because we don't want it to track + Span selectionSpan = TextRange.AdvancedTextRange; + bool isReversed = IsReversed; + bool isEmpty = IsEmpty; + if (!TextRange.ToggleCase()) + return false; + + if (!isEmpty) + { + TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, selectionSpan), isReversed); + } + + return true; + } + + public override bool Delete() + { + foreach (var span in TextSelection.SelectedSpans) + { + DisplayTextRange selectedRange = TextView.GetTextRange(span.Start, span.End); + if (!selectedRange.Delete()) + return false; + } + + return true; + } + + public override bool Indent() + { + bool singleLineSelection = (GetStartPoint().LineNumber == GetEndPoint().LineNumber); + bool entireLastLineSelected + = (GetStartPoint().CurrentPosition != GetEndPoint().CurrentPosition && + GetStartPoint().CurrentPosition == TextBuffer.GetEndPoint().StartOfLine && + GetEndPoint().CurrentPosition == TextBuffer.GetEndPoint().EndOfLine); + + if (singleLineSelection && !entireLastLineSelected) + { + TextPoint endPoint = GetEndPoint(); + if (!Delete()) + return false; + if (!endPoint.InsertIndent()) + return false; + TextView.AdvancedTextView.Caret.MoveTo(endPoint.AdvancedTextPoint); + } + else // indent the selected lines + { + VirtualSnapshotPoint oldStartPoint = TextSelection.Start; + VirtualSnapshotPoint oldEndPoint = TextSelection.End; + bool isReversed = TextSelection.IsReversed; + + ITextSnapshotLine startLine = AdvancedTextRange.Snapshot.GetLineFromPosition(oldStartPoint.Position); + ITextSnapshotLine endLine = AdvancedTextRange.Snapshot.GetLineFromPosition(oldEndPoint.Position); + + // If the selection span initially starts at the whitespace at the beginning of the line in the startLine or + // ends at the whitespace at the beginning of the line in the endLine, restore selection and caret position, + // *unless* the selection was in box mode. + bool startAtStartLineWhitespace = oldStartPoint.Position <= _textView.GetTextPoint(startLine.Start).GetFirstNonWhiteSpaceCharacterOnLine().CurrentPosition; + bool endAtEndLineWhitespace = oldEndPoint.Position < _textView.GetTextPoint(endLine.Start).GetFirstNonWhiteSpaceCharacterOnLine().CurrentPosition; + bool isBoxSelection = AdvancedSelection.Mode == TextSelectionMode.Box; + + if (isBoxSelection) + { + if (!this.BoxIndent()) + return false; + } + else + { + if (!TextRange.Indent()) + return false; + } + + // Computing the new selection and caret position + VirtualSnapshotPoint newStartPoint = TextSelection.Start; + VirtualSnapshotPoint newEndPoint = TextSelection.End; + + if (!isBoxSelection && (startAtStartLineWhitespace || endAtEndLineWhitespace)) + { + // After indent selection span should start at the start of startLine and end at the start of endLine + if (startAtStartLineWhitespace) + { + newStartPoint = new VirtualSnapshotPoint(AdvancedTextRange.Snapshot, oldStartPoint.Position.Position); + } + + if (endAtEndLineWhitespace && oldEndPoint.Position.Position != endLine.Start && endLine.Length != 0) + { + int insertedTextSize = _editorOptions.IsConvertTabsToSpacesEnabled() ? _editorOptions.GetTabSize() : 1; + newEndPoint = new VirtualSnapshotPoint(AdvancedTextRange.Snapshot, newEndPoint.Position.Position - insertedTextSize); + } + + if (!isReversed) + TextSelection.Select(newStartPoint, newEndPoint); + else + TextSelection.Select(newEndPoint, newStartPoint); + + TextView.AdvancedTextView.Caret.MoveTo(TextSelection.ActivePoint, PositionAffinity.Successor); + } + } + TextView.AdvancedTextView.Caret.EnsureVisible(); + return true; + } + + /// <summary> + /// Indent the given box selection + /// </summary> + /// <remarks> + /// This is fairly close to the normal text range indenting logic, except that it also + /// indents an empty selection at the endline, which the normal text range ignores. + /// </remarks> + private bool BoxIndent() + { + string textToInsert = _editorOptions.IsConvertTabsToSpacesEnabled() ? new string(' ', _editorOptions.GetTabSize()) : "\t"; + + using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit()) + { + ITextSnapshot snapshot = TextBuffer.AdvancedTextBuffer.CurrentSnapshot; + int startLineNumber = GetStartPoint().LineNumber; + int endLineNumber = GetEndPoint().LineNumber; + + for (int i = startLineNumber; i <= endLineNumber; i++) + { + ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i); + if (line.Length > 0) + { + if (!edit.Insert(line.Start, textToInsert)) + return false; + } + } + + edit.Apply(); + + if (edit.Canceled) + return false; + } + + return true; + } + + public override bool Unindent() + { + if (GetStartPoint().LineNumber != GetEndPoint().LineNumber && + AdvancedSelection.Mode == TextSelectionMode.Box) + { + if (!this.BoxUnindent()) + return false; + } + else + { + if (!TextRange.Unindent()) + return false; + } + + TextView.AdvancedTextView.Caret.EnsureVisible(); + return true; + } + + /// <summary> + /// Unindent the given box selection + /// </summary> + /// <remarks> + /// This is fairly close to the normal text range unindenting logic, except that it also + /// unindents an empty selection at the endline, which the normal text range ignores. + /// </remarks> + private bool BoxUnindent() + { + using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit()) + { + ITextSnapshot snapshot = TextBuffer.AdvancedTextBuffer.CurrentSnapshot; + int startLineNumber = GetStartPoint().LineNumber; + int endLineNumber = GetEndPoint().LineNumber; + + for (int i = startLineNumber; i <= endLineNumber; i++) + { + ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i); + if (line.Length > 0) + { + if (snapshot[line.Start] == '\t') + { + if (!edit.Delete(new Span(line.Start, 1))) + return false; + } + else + { + int spacesToRemove = 0; + for (; (line.Start + spacesToRemove < snapshot.Length) && (spacesToRemove < _editorOptions.GetTabSize()); + spacesToRemove++) + { + if (snapshot[line.Start + spacesToRemove] != ' ') + { + break; + } + } + + if (spacesToRemove > 0) + { + if (!edit.Delete(new Span(line.Start, spacesToRemove))) + return false; + } + } + } + } + + edit.Apply(); + + if (edit.Canceled) + return false; + } + + return true; + } + + public override TextRange Find(string pattern) + { + return TextRange.Find(pattern); + } + + public override TextRange Find(string pattern, FindOptions findOptions) + { + return TextRange.Find(pattern, findOptions); + } + + public override Collection<TextRange> FindAll(string pattern) + { + return TextRange.FindAll(pattern); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions) + { + return TextRange.FindAll(pattern, findOptions); + } + + public override bool ReplaceText(string newText) + { + // We use *int* and *Span* here because we don't want them to track + int startPosition = TextRange.GetDisplayStartPoint().CurrentPosition; + Span newSelectionSpan = new Span(startPosition, newText.Length); + bool isReversed = IsReversed; + if (!TextRange.ReplaceText(newText)) + return false; + TextSelection.Select(new SnapshotSpan(_textView.AdvancedTextView.TextSnapshot, newSelectionSpan), isReversed); + return true; + } + + public override string GetText() + { + return TextRange.GetText(); + } + + public override void SetStart(TextPoint startPoint) + { + this.SelectRange(startPoint.CurrentPosition, GetDisplayEndPoint().CurrentPosition); + } + + public override void SetEnd(TextPoint endPoint) + { + this.ExtendSelection(endPoint); + } + + public override void MoveTo(TextRange newRange) + { + this.SelectRange(newRange); + } + + protected override IEnumerator<TextPoint> GetEnumeratorInternal() + { + return ((TextRange)TextRange).GetEnumerator(); + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs new file mode 100644 index 0000000..be6d268 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultTextPointPrimitive.cs @@ -0,0 +1,1015 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Globalization; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Operations; + + using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + + internal sealed class DefaultTextPointPrimitive : TextPoint + { + private TextBuffer _textBuffer; + private ITrackingPoint _trackingPoint; + private IEditorOptions _editorOptions; + private ITextStructureNavigator _textStructureNavigator; + private ITextSearchService _findLogic; + private IBufferPrimitivesFactoryService _bufferPrimitivesFactory; + + public DefaultTextPointPrimitive( + TextBuffer textBuffer, + int position, + ITextSearchService findLogic, + IEditorOptions editorOptions, + ITextStructureNavigator textStructureNavigator, + IBufferPrimitivesFactoryService bufferPrimitivesFactory) + { + if ((position < 0) || + (position > textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length)) + { + throw new ArgumentOutOfRangeException("position"); + } + + _textBuffer = textBuffer; + _trackingPoint = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.CreateTrackingPoint(0, PointTrackingMode.Positive); + _editorOptions = editorOptions; + _textStructureNavigator = textStructureNavigator; + _findLogic = findLogic; + _bufferPrimitivesFactory = bufferPrimitivesFactory; + + MoveTo(position); + } + + public override TextBuffer TextBuffer + { + get { return _textBuffer; } + } + + public override int CurrentPosition + { + get { return _trackingPoint.GetPosition(_textBuffer.AdvancedTextBuffer.CurrentSnapshot); } + } + + public override int Column + { + get + { + SnapshotPoint currentLocation = _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot); + + ITextSnapshotLine line = currentLocation.GetContainingLine(); + + if (line.Start != currentLocation.Position) + { + string lineText = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetText(Span.FromBounds(line.Start, currentLocation.Position)); + + StringInfo lineTextInfo = new StringInfo(lineText); + + int column = 0; + for (int i = 0; i < lineTextInfo.LengthInTextElements; i++) + { + string textElement = lineTextInfo.SubstringByTextElements(i, 1); + if (textElement == "\t") + { + // If there is a tab in the text, then the column automatically jumps + // to the next tab stop. + int tabSize = _editorOptions.GetTabSize(); + column = ((column / tabSize) + 1) * tabSize; + } + else + { + column++; + } + } + + return column; + } + + return 0; + } + } + + public override bool DeleteNext() + { + string nextCharacter = GetNextCharacter(); + return PrimitivesUtilities.Delete(_textBuffer.AdvancedTextBuffer, CurrentPosition, nextCharacter.Length); + } + + public override bool DeletePrevious() + { + ITextSnapshot snapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot; + int previousPosition = CurrentPosition - 1; + + if (previousPosition < 0) + { + return true; + } + + int index = previousPosition; + char currentCharacter = snapshot[previousPosition]; + + // By default VS (and many other apps) will delete only the last character + // of a combining character sequence. The one exception to this rule is + // surrogate pais which we are handling here. + if (char.GetUnicodeCategory(currentCharacter) == UnicodeCategory.Surrogate) + { + index--; + } + + if (index > 0) + { + if (currentCharacter == '\n') + { + if (snapshot[previousPosition - 1] == '\r') + { + index--; + } + } + } + + return PrimitivesUtilities.Delete(_textBuffer.AdvancedTextBuffer, index, previousPosition - index + 1); + } + + public override TextRange GetCurrentWord() + { + // If the line is blank, just return a blank range for the line + SnapshotPoint currentPoint = AdvancedTextPoint; + ITextSnapshotLine line = currentPoint.GetContainingLine(); + if (currentPoint == line.Start && line.Length == 0) + { + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this, this); + } + + TextExtent textExtent = GetTextExtent(CurrentPosition); + + // If the word is not significant, then see if there is a significant word right before this one, + // and return that instead to mimic VS 9 behavior. + if (!textExtent.IsSignificant) + { + if ((textExtent.Span.Start > 0) && (textExtent.Span.Length > 0)) + { + if (textExtent.Span.Start == CurrentPosition) + { + if (!char.IsWhiteSpace(_textBuffer.AdvancedTextBuffer.CurrentSnapshot[textExtent.Span.Start - 1])) + { + textExtent = GetTextExtent(textExtent.Span.Start - 1); + } + } + else if (CurrentPosition == EndOfLine) + { + // If this text point is on the end of the line, then check to see if there is a word + // just before the whitespace. + if (!char.IsWhiteSpace(_textBuffer.AdvancedTextBuffer.CurrentSnapshot[textExtent.Span.Start - 1])) + { + TextExtent newExtent = new TextExtent(GetTextExtent(textExtent.Span.Start - 1)); + textExtent = new TextExtent(new SnapshotSpan(newExtent.Span.Start, textExtent.Span.End), true); + } + } + } + } + + TextPoint startPoint = Clone(); + startPoint.MoveTo(textExtent.Span.Start); + TextPoint endPoint = Clone(); + endPoint.MoveTo(textExtent.Span.End); + + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, startPoint, endPoint); + } + + private TextExtent GetTextExtent(int position) + { + return _textStructureNavigator.GetExtentOfWord(new SnapshotPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot, position)); + } + + public override TextRange GetNextWord() + { + TextExtent currentWord = GetTextExtent(CurrentPosition); + + if (currentWord.Span.End < _textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length) + { + // If the current point is at the end of the line, look for the next word on the next line + if ((CurrentPosition == EndOfLine) && (CurrentPosition != _textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length)) + { + TextPoint textPoint = Clone(); + textPoint.MoveToBeginningOfNextLine(); + textPoint.MoveTo(textPoint.StartOfLine); + TextExtent wordOnNextLine = GetTextExtent(textPoint.CurrentPosition); + if (wordOnNextLine.IsSignificant) + { + return textPoint.GetCurrentWord(); + } + else if (wordOnNextLine.Span.End >= textPoint.EndOfLine) + { + return textPoint.GetTextRange(textPoint.EndOfLine); + } + return textPoint.GetNextWord(); + } + // By default, VS stops at line breaks when determing word + // boundaries. + else if (ShouldStopAtEndOfLine(currentWord.Span.End) || IsCurrentWordABlankLine(currentWord)) + { + return GetTextRange(currentWord.Span.End); + } + + TextExtent nextWord = GetTextExtent(currentWord.Span.End); + + if (!nextWord.IsSignificant) + { + nextWord = GetTextExtent(nextWord.Span.End); + } + + int start = nextWord.Span.Start; + int end = nextWord.Span.End; + + // The text structure navigator can return a word with whitespace attached at the end. + // Handle that case here. + start = Math.Max(start, currentWord.Span.End); + + TextPoint startPoint = Clone(); + TextPoint endPoint = Clone(); + startPoint.MoveTo(start); + endPoint.MoveTo(end); + + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, startPoint, endPoint); + } + + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, TextBuffer.GetEndPoint(), TextBuffer.GetEndPoint()); + } + + public override TextRange GetPreviousWord() + { + TextRange currentWord = GetCurrentWord(); + + if (currentWord.GetStartPoint().CurrentPosition > 0) + { + // By default, VS stops at line breaks when determing word + // boundaries. + if ((currentWord.GetStartPoint().CurrentPosition == StartOfLine) && + (CurrentPosition != StartOfLine)) + { + return GetTextRange(currentWord.GetStartPoint()); + } + + // If the point is at the end of a word that is not whitespace, it is possible + // that the "current word" is also the previous word in standard VS. + if ((currentWord.GetEndPoint().CurrentPosition == CurrentPosition) && + (!currentWord.IsEmpty)) + { + return currentWord; + } + + TextPoint pointInPreviousWord = currentWord.GetStartPoint(); + pointInPreviousWord.MoveTo(pointInPreviousWord.CurrentPosition - 1); + + TextRange previousWord = pointInPreviousWord.GetCurrentWord(); + + if (previousWord.GetStartPoint().CurrentPosition > 0) + { + if (ShouldContinuePastPreviousWord(previousWord)) + { + pointInPreviousWord.MoveTo(previousWord.GetStartPoint().CurrentPosition - 1); + previousWord = pointInPreviousWord.GetCurrentWord(); + } + } + + return previousWord; + } + + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, TextBuffer.GetStartPoint(), TextBuffer.GetStartPoint()); + } + + public override TextRange GetTextRange(TextPoint otherPoint) + { + if (otherPoint == null) + { + throw new ArgumentNullException("otherPoint"); + } + + if (otherPoint.TextBuffer != TextBuffer) + { + throw new ArgumentException(Strings.OtherPointFromWrongBuffer); + } + + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this.Clone(), otherPoint); + } + + public override TextRange GetTextRange(int otherPosition) + { + if ((otherPosition < 0) || (otherPosition > TextBuffer.AdvancedTextBuffer.CurrentSnapshot.Length)) + { + throw new ArgumentOutOfRangeException("otherPosition"); + } + + TextPoint otherPoint = this.Clone(); + otherPoint.MoveTo(otherPosition); + + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this.Clone(), otherPoint); + } + + public override bool InsertNewLine() + { + string lineBreak = null; + if (_editorOptions.GetReplicateNewLineCharacter()) + { + ITextSnapshot snapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot; + int position = _trackingPoint.GetPosition(snapshot); + ITextSnapshotLine currentSnapshotLine = snapshot.GetLineFromPosition(position); + if (currentSnapshotLine.LineBreakLength > 0) + { + // use the same line ending as the current line + lineBreak = currentSnapshotLine.GetLineBreakText(); + } + else + { + // we are on the last line of the buffer + if (snapshot.LineCount > 1) + { + // use the same line ending as the penultimate line in the buffer + lineBreak = snapshot.GetLineFromLineNumber(snapshot.LineCount - 2).GetLineBreakText(); + } + } + } + return InsertText(lineBreak ?? _editorOptions.GetNewLineCharacter()); + } + + public override bool InsertIndent() + { + int tabSize = _editorOptions.GetTabSize(); + int spacesToInsert = tabSize - (Column % tabSize); + string indentToInsert = _editorOptions.IsConvertTabsToSpacesEnabled() ? new string(' ', spacesToInsert) : "\t"; + + return InsertText(indentToInsert); + } + + public override bool InsertText(string text) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + + if (text.Length > 0) + { + return PrimitivesUtilities.Insert(_textBuffer.AdvancedTextBuffer, _trackingPoint.GetPosition(_textBuffer.AdvancedTextBuffer.CurrentSnapshot), text); + } + else + { + return true; + } + } + + public override int LineNumber + { + get + { + return _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot).GetContainingLine().LineNumber; + } + } + + public override int StartOfLine + { + get + { + return _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot).GetContainingLine().Start; + } + } + + public override int EndOfLine + { + get + { + return _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot).GetContainingLine().End; + } + } + + public override bool RemovePreviousIndent() + { + if (Column > 0) + { + int tabSize = _editorOptions.GetTabSize(); + + int previousTabStop = Column - tabSize; + if (Column % tabSize > 0) + { + previousTabStop = (Column / tabSize) * tabSize; + } + + int positionToDeleteTo = CurrentPosition; + + TextPoint newPoint = Clone(); + for (int i = CurrentPosition - 1; newPoint.Column >= previousTabStop; i--) + { + newPoint.MoveTo(i); + string character = newPoint.GetNextCharacter(); + if (character != " " && character != "\t") + { + break; + } + + positionToDeleteTo = i; + + if (newPoint.Column == previousTabStop) + { + break; + } + } + + return PrimitivesUtilities.Delete(_textBuffer.AdvancedTextBuffer, Span.FromBounds(positionToDeleteTo, CurrentPosition)); + } + else + { + return true; + } + } + + public override bool TransposeCharacter() + { + int insertionIndex = CurrentPosition; + + ITextSnapshotLine line = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromPosition(insertionIndex); + + string lineText = line.GetText(); + + if (StringInfo.ParseCombiningCharacters(lineText).Length < 2) + { + return true; + } + + Span textElementLeftSpan, textElementRightSpan; + string textElementLeft, textElementRight; + int linePosition = CurrentPosition - StartOfLine; + + // We're at the start of a line + if (insertionIndex == line.Start) + { + textElementLeft = StringInfo.GetNextTextElement(lineText, linePosition); + textElementLeftSpan = new Span(linePosition + line.Start, textElementLeft.Length); + textElementRight = StringInfo.GetNextTextElement(lineText, linePosition + textElementLeft.Length); + textElementRightSpan = new Span(textElementLeftSpan.End, textElementRight.Length); + } + // We're at the end of a line + else if (insertionIndex == line.End) + { + textElementRight = StringInfo.GetNextTextElement(lineText, linePosition - 1); + textElementRightSpan = new Span(linePosition - 1 + line.Start, textElementRight.Length); + textElementLeft = StringInfo.GetNextTextElement(lineText, textElementRightSpan.Start - line.Start - 1); + textElementLeftSpan = new Span(textElementRightSpan.Start - 1, textElementRight.Length); + } + // We're at the middle of a line + else + { + textElementRight = StringInfo.GetNextTextElement(lineText, linePosition); + textElementRightSpan = new Span(linePosition + line.Start, textElementRight.Length); + textElementLeft = StringInfo.GetNextTextElement(lineText, textElementRightSpan.Start - line.Start - 1); + textElementLeftSpan = new Span(textElementRightSpan.Start - 1, textElementRight.Length); + } + + string transposedText = textElementRight + textElementLeft; + + return PrimitivesUtilities.Replace(_textBuffer.AdvancedTextBuffer, new Span(textElementLeftSpan.Start, transposedText.Length), transposedText); + } + + public override bool TransposeLine() + { + ITextSnapshot currentSnapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot; + + // If there is only a single line in this buffer, don't do anything. + if (currentSnapshot.LineCount <= 1) + { + return true; + } + + ITextSnapshotLine currentLine = currentSnapshot.GetLineFromPosition(CurrentPosition); + + ITextSnapshotLine nextLine = null; + + if (currentLine.LineNumber == currentSnapshot.LineCount - 1) + { + // If the caret is on the last line of the buffer, transpose the next to last line + // with the last one. + nextLine = currentSnapshot.GetLineFromLineNumber(currentLine.LineNumber - 1); + } + else + { + nextLine = currentSnapshot.GetLineFromLineNumber(currentLine.LineNumber + 1); + } + + return TransposeLine(nextLine.LineNumber); + } + + public override bool TransposeLine(int lineNumber) + { + if ((lineNumber < 0) || (lineNumber > _textBuffer.AdvancedTextBuffer.CurrentSnapshot.LineCount)) + { + throw new ArgumentOutOfRangeException("lineNumber"); + } + + ITextSnapshot currentSnapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot; + + // If there is only a single line in this buffer, don't do anything. + if (currentSnapshot.LineCount <= 1) + { + return true; + } + + ITextSnapshotLine currentLine = currentSnapshot.GetLineFromPosition(CurrentPosition); + ITextSnapshotLine nextLine = currentSnapshot.GetLineFromLineNumber(lineNumber); + + using (ITextEdit edit = _textBuffer.AdvancedTextBuffer.CreateEdit()) + { + // Make sure that both replaces succeed before applying + if ((edit.Replace(currentLine.Extent, nextLine.GetText())) && + (edit.Replace(nextLine.Extent, currentLine.GetText()))) + { + edit.Apply(); + if (edit.Canceled) + return false; + } + else + { + edit.Cancel(); + return false; + } + } + + int newLineNumber = nextLine.LineNumber; + if (currentLine.LineNumber == currentSnapshot.LineCount - 1) + { + newLineNumber = currentLine.LineNumber; + } + + MoveTo(_textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromLineNumber(newLineNumber).Start); + + return true; + } + + public override SnapshotPoint AdvancedTextPoint + { + get { return _trackingPoint.GetPoint(_textBuffer.AdvancedTextBuffer.CurrentSnapshot); } + } + + public override string GetNextCharacter() + { + ITextSnapshot snapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot; + int currentPosition = CurrentPosition; + + if (currentPosition == snapshot.Length) + { + return string.Empty; + } + + int index = currentPosition; + string characterText = char.ToString(snapshot[currentPosition]); + + while (++index < snapshot.Length) + { + string newText = characterText + snapshot[index]; + if (StringInfo.GetNextTextElement(newText).Length <= characterText.Length) + break; + + characterText = newText; + } + + return characterText; + } + + public override string GetPreviousCharacter() + { + ITextSnapshot snapshot = _textBuffer.AdvancedTextBuffer.CurrentSnapshot; + int currentPosition = CurrentPosition; + + if (currentPosition == 0) + { + return string.Empty; + } + + int index = currentPosition - 1; + string characterText = char.ToString(snapshot[index]); + + while (--index >= 0) + { + string newText = snapshot[index] + characterText; + if (StringInfo.GetNextTextElement(newText).Length <= characterText.Length) + break; + + characterText = newText; + } + + return characterText; + } + + public override TextRange Find(string pattern, TextPoint endPoint) + { + ValidateFindParameters(pattern, endPoint); + return Find(pattern, FindOptions.None, endPoint); + } + + public override TextRange Find(string pattern) + { + ValidateFindParameters(pattern, this); + return Find(pattern, FindOptions.None); + } + + public override TextRange Find(string pattern, FindOptions findOptions) + { + ValidateFindParameters(pattern, this); + return FindMatch(pattern, findOptions, null); + } + + public override TextRange Find(string pattern, FindOptions findOptions, TextPoint endPoint) + { + ValidateFindParameters(pattern, endPoint); + return FindMatch(pattern, findOptions, endPoint); + } + + private TextRange FindMatch(string pattern, FindOptions findOptions, TextPoint endPoint) + { + if (pattern.Length == 0) + { + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this.Clone(), this.Clone()); + } + + FindData findData = new FindData(pattern, _textBuffer.AdvancedTextBuffer.CurrentSnapshot); + findData.FindOptions = findOptions; + + bool wrapAround = endPoint == null; + + bool searchReverse = ((findData.FindOptions & FindOptions.SearchReverse) == FindOptions.SearchReverse); + + // In the case of a wrap-around search, we can start the search at this point always. In the case + // where we are searching a range in reverse, we have to start at the end point. + int searchStartPosition = (searchReverse && !wrapAround) ? endPoint.CurrentPosition : CurrentPosition; + + SnapshotSpan? snapshotSpan = _findLogic.FindNext(searchStartPosition, wrapAround, findData); + + if (snapshotSpan.HasValue) + { + if ((endPoint == null) || (snapshotSpan.Value.End <= endPoint.CurrentPosition)) + { + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, + _bufferPrimitivesFactory.CreateTextPoint(_textBuffer, snapshotSpan.Value.Start), + _bufferPrimitivesFactory.CreateTextPoint(_textBuffer, snapshotSpan.Value.End)); + } + } + + return _bufferPrimitivesFactory.CreateTextRange(_textBuffer, this.Clone(), this.Clone()); + } + + public override Collection<TextRange> FindAll(string pattern, TextPoint endPoint) + { + return FindAll(pattern, FindOptions.None, endPoint); + } + + public override Collection<TextRange> FindAll(string pattern) + { + return FindAll(pattern, FindOptions.None); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions) + { + ValidateFindParameters(pattern, this); + + if (pattern.Length == 0) + { + return new Collection<TextRange>(); + } + + FindData findData = new FindData(pattern, _textBuffer.AdvancedTextBuffer.CurrentSnapshot); + findData.FindOptions = findOptions; + + // Assume that FindAll returns matches sorted starting at the beginning + // of the snapshot. + Collection<SnapshotSpan> matches = _findLogic.FindAll(findData); + + List<TextRange> ranges = new List<TextRange>(); + Collection<TextRange> beforeRanges = new Collection<TextRange>(); + + foreach (SnapshotSpan span in matches) + { + TextRange textRange = _bufferPrimitivesFactory.CreateTextRange(_textBuffer, + _bufferPrimitivesFactory.CreateTextPoint(_textBuffer, span.Start), + _bufferPrimitivesFactory.CreateTextPoint(_textBuffer, span.End)); + + if (textRange.GetStartPoint().CurrentPosition < CurrentPosition) + { + beforeRanges.Add(textRange); + } + else + { + ranges.Add(textRange); + } + } + + ranges.AddRange(beforeRanges); + + return new Collection<TextRange>(ranges); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions, TextPoint endPoint) + { + ValidateFindParameters(pattern, endPoint); + + Collection<TextRange> matchingRanges = new Collection<TextRange>(); + Collection<TextRange> textRanges = FindAll(pattern, findOptions); + + foreach (TextRange textRange in textRanges) + { + if ((textRange.GetStartPoint().CurrentPosition >= CurrentPosition) && + (textRange.GetEndPoint().CurrentPosition <= endPoint.CurrentPosition)) + { + matchingRanges.Add(textRange); + } + } + + return matchingRanges; + } + + private void ValidateFindParameters(string pattern, TextPoint endPoint) + { + if (pattern == null) + { + throw new ArgumentNullException("pattern"); + } + if (endPoint == null) + { + throw new ArgumentNullException("endPoint"); + } + if (endPoint.TextBuffer != TextBuffer) + { + throw new ArgumentException(Strings.OtherPointFromWrongBuffer); + } + } + + public override void MoveTo(int position) + { + position = FindStartOfCombiningSequence(_textBuffer.AdvancedTextBuffer.CurrentSnapshot, position); + + _trackingPoint = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.CreateTrackingPoint(position, PointTrackingMode.Positive); + } + + /// <summary> + /// Given a snapshot and position, find the start of the first combining sequence that is less + /// than or equal to the given position. In the 99% case, the position is already at the start + /// of a sequence, but we want to make sure it isn't in the middle of a surrogate pair or between + /// a base character/surrogate pair and its associated combining characters (which may also + /// be surrogate pairs). + /// </summary> + /// <returns>The start of position of the combining sequence, usually <paramref name="position"/> again.</returns> + private static int FindStartOfCombiningSequence(ITextSnapshot snapshot, int position) + { + if ((position < 0) || + (position > snapshot.Length)) + { + throw new ArgumentOutOfRangeException("position"); + } + + // If this is the end of the snapshot, we don't need to check anything. + if (position == snapshot.Length) + return position; + + // 99% case: + // Most of the time, the given character is neither a combining character nor a surrogate codepoint. + // In that case, we don't need to walk around the snapshot looking for the start of a combining + // character sequence. + UnicodeCategory categoryAtPosition = CharUnicodeInfo.GetUnicodeCategory(snapshot[position]); + if (!IsPotentialCombiningCharacter(categoryAtPosition)) + return position; + + // If we're at the start of a line, we're already in a good position. + var line = snapshot.GetLineFromPosition(position); + if (position == line.Start) + return position; + + // If we've made it all the way here, we have a potential combining character sequence. + // We've already determined that the current character is a potential combining character, + // so start at one character previous. + int endPosition = position + 1; + int lineStart = line.Start; + position--; + + for (; position > lineStart; position--) + { + char ch = snapshot[position]; + UnicodeCategory category = CharUnicodeInfo.GetUnicodeCategory(ch); + + // We can skip potential combining characters (we'll gather them all up once we + // hit a base character). + if (IsPotentialCombiningCharacter(category)) + { + continue; + } + + // At this point, the character is likely a base. Use ParseCombiningCharacters to find the sequences + // in the text up to this point. + + string upToCurrentPosition = snapshot.GetText(Span.FromBounds(position, endPosition)); + int[] sequences = StringInfo.ParseCombiningCharacters(upToCurrentPosition); + + // If we've found more than one combining character sequence, use the last one as our + // position and stop looping. + if (sequences.Length > 1) + { + return (position) + sequences[sequences.Length - 1]; + } + } + + // If we walked the position back to the start of the line, do one last combining character sequence check and use the result of that + if (position == line.Start) + { + string upToCurrentPosition = snapshot.GetText(Span.FromBounds(position, endPosition)); + int[] sequences = StringInfo.ParseCombiningCharacters(upToCurrentPosition); + if (sequences.Length > 1) + { + return position + sequences[sequences.Length - 1]; + } + } + + return position; + } + + /// <summary> + /// Determine if the character is potentially a part of a combining sequence. This includes + /// the known combining character classes (M) and surrogates, as the MSDN documentation for + /// .NET Unicode support notes that it may consider surrogates to be combining characters. + /// </summary> + private static bool IsPotentialCombiningCharacter(UnicodeCategory category) + { + return category == UnicodeCategory.EnclosingMark || + category == UnicodeCategory.NonSpacingMark || + category == UnicodeCategory.SpacingCombiningMark || + category == UnicodeCategory.Surrogate; + } + + public override void MoveToNextCharacter() + { + string nextCharacter = GetNextCharacter(); + MoveTo(CurrentPosition + nextCharacter.Length); + } + + public override void MoveToPreviousCharacter() + { + string previousCharacter = GetPreviousCharacter(); + + MoveTo(CurrentPosition - previousCharacter.Length); + } + + protected override TextPoint CloneInternal() + { + return new DefaultTextPointPrimitive(_textBuffer, CurrentPosition, _findLogic, _editorOptions, _textStructureNavigator, _bufferPrimitivesFactory); + } + + public override void MoveToLine(int lineNumber) + { + if ((lineNumber < 0) || + (lineNumber > _textBuffer.AdvancedTextBuffer.CurrentSnapshot.LineCount)) + { + throw new ArgumentOutOfRangeException("lineNumber"); + } + + ITextSnapshotLine line = _textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber); + + MoveTo(line.Start); + } + + public override void MoveToEndOfLine() + { + MoveTo(EndOfLine); + } + + public override void MoveToStartOfLine() + { + int firstNonWhitespaceCharacter = GetFirstNonWhiteSpaceCharacterOnLine().CurrentPosition; + int currentPosition = CurrentPosition; + int startOfLine = StartOfLine; + if ((currentPosition == firstNonWhitespaceCharacter) || + (firstNonWhitespaceCharacter == EndOfLine)) + { + MoveTo(startOfLine); + } + else if ((currentPosition > firstNonWhitespaceCharacter) || + (currentPosition == 0)) + { + MoveTo(firstNonWhitespaceCharacter); + } + } + + public override void MoveToEndOfDocument() + { + MoveTo(_textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length); + } + + public override void MoveToStartOfDocument() + { + MoveTo(0); + } + + public override void MoveToBeginningOfNextLine() + { + MoveTo(AdvancedTextPoint.GetContainingLine().EndIncludingLineBreak); + } + + public override void MoveToBeginningOfPreviousLine() + { + ITextSnapshotLine line = AdvancedTextPoint.GetContainingLine(); + + if (line.LineNumber != 0) + { + MoveTo(_textBuffer.AdvancedTextBuffer.CurrentSnapshot.GetLineFromLineNumber(line.LineNumber - 1).Start); + } + else + { + MoveToStartOfLine(); + } + } + + public override void MoveToNextWord() + { + if (CurrentPosition != _textBuffer.AdvancedTextBuffer.CurrentSnapshot.Length) + { + TextRange nextWord = GetNextWord(); + + if (nextWord.GetStartPoint().CurrentPosition == CurrentPosition) + { + MoveTo(nextWord.GetEndPoint().CurrentPosition); + } + else + { + MoveTo(nextWord.GetStartPoint().CurrentPosition); + } + } + } + + public override void MoveToPreviousWord() + { + if (CurrentPosition != 0) + { + TextRange currentWord = GetCurrentWord(); + if (CurrentPosition != currentWord.GetStartPoint().CurrentPosition) + { + MoveTo(currentWord.GetStartPoint().CurrentPosition); + } + else + { + TextRange previousWord = GetPreviousWord(); + + MoveTo(previousWord.GetStartPoint().CurrentPosition); + } + } + } + + public override TextPoint GetFirstNonWhiteSpaceCharacterOnLine() + { + int firstNonWhitespaceCharacterOnLine = StartOfLine; + for (; firstNonWhitespaceCharacterOnLine < EndOfLine; firstNonWhitespaceCharacterOnLine++) + { + if (!char.IsWhiteSpace(AdvancedTextPoint.Snapshot[firstNonWhitespaceCharacterOnLine])) + { + break; + } + } + return _bufferPrimitivesFactory.CreateTextPoint(TextBuffer, firstNonWhitespaceCharacterOnLine); + } + + /// <summary> + /// Determine if this text point's current word is a blank line on the next line. + /// </summary> + /// <param name="currentWord">The current word to check.</param> + private bool IsCurrentWordABlankLine(TextExtent currentWord) + { + TextPoint endOfCurrentWord = Clone(); + endOfCurrentWord.MoveTo(currentWord.Span.End); + return (EndOfLine == CurrentPosition) && (currentWord.Span.End == endOfCurrentWord.EndOfLine); + } + + /// <summary> + /// Determine if a the next word should stop at the end of the line. + /// </summary> + private bool ShouldStopAtEndOfLine(int endOfWord) + { + // If the current word ends at the end of the line and the current position is not the end of the line, + // then the word movement should stop at the end of the line. + return (endOfWord == EndOfLine) && (EndOfLine > CurrentPosition); + } + + /// <summary> + /// Determine if the checking of the previous word should go past the previous word. + /// </summary> + /// <param name="previousWord">The current previous word.</param> + private static bool ShouldContinuePastPreviousWord(TextRange previousWord) + { + // If the previous word is whitespace, and the previous word is not a blank line + // then it should be included in the previous word. + return char.IsWhiteSpace(previousWord.GetStartPoint().GetNextCharacter()[0]) && + previousWord.GetStartPoint().CurrentPosition != previousWord.GetStartPoint().StartOfLine; + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs new file mode 100644 index 0000000..961db4f --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultTextRangePrimitive.cs @@ -0,0 +1,380 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Globalization; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Operations; + + using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods; + + internal sealed class DefaultTextRangePrimitive : TextRange + { + private TextPoint _startPoint; + private TextPoint _endPoint; + private IEditorOptions _editorOptions; + private IEditorOptionsFactoryService _editorOptionsProvider; + + internal DefaultTextRangePrimitive(TextPoint startPoint, TextPoint endPoint, IEditorOptionsFactoryService editorOptionsProvider) + { + if (startPoint.CurrentPosition < endPoint.CurrentPosition) + { + _startPoint = startPoint.Clone(); + _endPoint = endPoint.Clone(); + } + else + { + _endPoint = startPoint.Clone(); + _startPoint = endPoint.Clone(); + } + _editorOptionsProvider = editorOptionsProvider; + _editorOptions = _editorOptionsProvider.GetOptions(_startPoint.TextBuffer.AdvancedTextBuffer); + } + + public override TextPoint GetStartPoint() + { + return _startPoint.Clone(); + } + + public override TextPoint GetEndPoint() + { + return _endPoint.Clone(); + } + + public override TextBuffer TextBuffer + { + get { return _startPoint.TextBuffer; } + } + + public override SnapshotSpan AdvancedTextRange + { + get { return new SnapshotSpan(TextBuffer.AdvancedTextBuffer.CurrentSnapshot, Span.FromBounds(_startPoint.CurrentPosition, _endPoint.CurrentPosition)); } + } + + public override bool IsEmpty + { + get { return _startPoint.CurrentPosition == _endPoint.CurrentPosition; } + } + + public override bool MakeUppercase() + { + return ReplaceText(GetText().ToUpper(CultureInfo.CurrentCulture)); + } + + public override bool MakeLowercase() + { + return ReplaceText(GetText().ToLower(CultureInfo.CurrentCulture)); + } + + public override bool Capitalize() + { + int startPosition = _startPoint.CurrentPosition; + if (IsEmpty) + { + int endPosition = _endPoint.CurrentPosition; + TextRange currentWord = _startPoint.GetCurrentWord(); + string nextCharacter = _startPoint.GetNextCharacter(); + if (_startPoint.CurrentPosition == currentWord.GetStartPoint().CurrentPosition) + { + nextCharacter = nextCharacter.ToUpper(CultureInfo.CurrentCulture); + } + else + { + nextCharacter = nextCharacter.ToLower(CultureInfo.CurrentCulture); + } + if (!PrimitivesUtilities.Replace(TextBuffer.AdvancedTextBuffer, new Span(_startPoint.CurrentPosition, nextCharacter.Length), nextCharacter)) + return false; + _endPoint.MoveTo(endPosition); + } + else + { + using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit()) + { + TextRange currentWord = _startPoint.GetCurrentWord(); + + // If the current word extends past this range, go to the next word + if (currentWord.GetStartPoint().CurrentPosition < _startPoint.CurrentPosition) + { + currentWord = currentWord.GetEndPoint().GetNextWord(); + } + + while (currentWord.GetStartPoint().CurrentPosition < _endPoint.CurrentPosition) + { + string wordText = currentWord.GetText(); + string startElement = StringInfo.GetNextTextElement(wordText); + wordText = startElement.ToUpper(CultureInfo.CurrentCulture) + wordText.Substring(startElement.Length).ToLower(CultureInfo.CurrentCulture); + if (!edit.Replace(currentWord.AdvancedTextRange.Span, wordText)) + { + edit.Cancel(); + return false; + } + + currentWord = currentWord.GetEndPoint().GetNextWord(); + } + + edit.Apply(); + + if (edit.Canceled) + return false; + } + } + _startPoint.MoveTo(startPosition); + return true; + } + + public override bool ToggleCase() + { + if (IsEmpty) + { + TextPoint nextPoint = _startPoint.Clone(); + nextPoint.MoveToNextCharacter(); + TextRange nextCharacter = _startPoint.GetTextRange(nextPoint); + string nextCharacterString = nextCharacter.GetText(); + if (char.IsUpper(nextCharacterString, 0)) + { + nextCharacterString = nextCharacterString.ToLower(CultureInfo.CurrentCulture); + } + else + { + nextCharacterString = nextCharacterString.ToUpper(CultureInfo.CurrentCulture); + } + return nextCharacter.ReplaceText(nextCharacterString); + } + else + { + int startPosition = _startPoint.CurrentPosition; + using (ITextEdit textEdit = TextBuffer.AdvancedTextBuffer.CreateEdit()) + { + for (int i = _startPoint.CurrentPosition; i < _endPoint.CurrentPosition; i++) + { + char newChar = textEdit.Snapshot[i]; + if (char.IsUpper(newChar)) + { + newChar = char.ToLower(newChar, CultureInfo.CurrentCulture); + } + else + { + newChar = char.ToUpper(newChar, CultureInfo.CurrentCulture); + } + + if (!textEdit.Replace(i, 1, newChar.ToString())) + { + textEdit.Cancel(); + return false; // break out early if any edit fails to reduce the time of the failure case + } + } + + textEdit.Apply(); + + if (textEdit.Canceled) + return false; + } + _startPoint.MoveTo(startPosition); + } + return true; + } + + public override bool Delete() + { + return PrimitivesUtilities.Delete(TextBuffer.AdvancedTextBuffer, Span.FromBounds(_startPoint.CurrentPosition, _endPoint.CurrentPosition)); + } + + public override bool Indent() + { + string textToInsert = _editorOptions.IsConvertTabsToSpacesEnabled() ? new string(' ', _editorOptions.GetTabSize()) : "\t"; + + if (_startPoint.LineNumber == _endPoint.LineNumber) + { + return _startPoint.InsertIndent(); + } + + using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit()) + { + ITextSnapshot snapshot = TextBuffer.AdvancedTextBuffer.CurrentSnapshot; + for (int i = _startPoint.LineNumber; i <= _endPoint.LineNumber; i++) + { + ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i); + if ((line.Length > 0) && + (line.Start != _endPoint.CurrentPosition)) + { + if (!edit.Insert(line.Start, textToInsert)) + return false; + } + } + + edit.Apply(); + + if (edit.Canceled) + return false; + } + + return true; + } + + public override bool Unindent() + { + if (_startPoint.LineNumber == _endPoint.LineNumber) + { + return _startPoint.RemovePreviousIndent(); + } + + using (ITextEdit edit = TextBuffer.AdvancedTextBuffer.CreateEdit()) + { + ITextSnapshot snapshot = TextBuffer.AdvancedTextBuffer.CurrentSnapshot; + + for (int i = _startPoint.LineNumber; i <= _endPoint.LineNumber; i++) + { + ITextSnapshotLine line = snapshot.GetLineFromLineNumber(i); + if ((line.Length > 0) && (_endPoint.CurrentPosition != line.Start)) + { + if (snapshot[line.Start] == '\t') + { + if (!edit.Delete(new Span(line.Start, 1))) + return false; + } + else + { + int spacesToRemove = 0; + for (; (line.Start + spacesToRemove < snapshot.Length) && (spacesToRemove < _editorOptions.GetTabSize()); + spacesToRemove++) + { + if (snapshot[line.Start + spacesToRemove] != ' ') + { + break; + } + } + + if (spacesToRemove > 0) + { + if (!edit.Delete(new Span(line.Start, spacesToRemove))) + return false; + } + } + } + } + + edit.Apply(); + + if (edit.Canceled) + return false; + } + + return true; + } + + public override TextRange Find(string pattern) + { + return Find(pattern, FindOptions.None); + } + + public override TextRange Find(string pattern, FindOptions findOptions) + { + return _startPoint.Find(pattern, findOptions, _endPoint); + } + + public override Collection<TextRange> FindAll(string pattern) + { + return FindAll(pattern, FindOptions.None); + } + + public override Collection<TextRange> FindAll(string pattern, FindOptions findOptions) + { + return _startPoint.FindAll(pattern, findOptions, _endPoint); + } + + public override bool ReplaceText(string newText) + { + if (string.IsNullOrEmpty(newText)) + { + throw new ArgumentNullException("newText"); + } + + int startPoint = _startPoint.CurrentPosition; + + if (!PrimitivesUtilities.Replace(TextBuffer.AdvancedTextBuffer, Span.FromBounds(_startPoint.CurrentPosition, _endPoint.CurrentPosition), newText)) + return false; + + _startPoint.MoveTo(startPoint); + + return true; + } + + public override string GetText() + { + return TextBuffer.AdvancedTextBuffer.CurrentSnapshot.GetText(Span.FromBounds(_startPoint.CurrentPosition, _endPoint.CurrentPosition)); + } + + protected override TextRange CloneInternal() + { + return new DefaultTextRangePrimitive(_startPoint, _endPoint, _editorOptionsProvider); + } + + public override void SetStart(TextPoint startPoint) + { + if (startPoint.TextBuffer != TextBuffer) + { + throw new ArgumentException(Strings.StartPointFromWrongBuffer); + } + + if (startPoint.CurrentPosition > _endPoint.CurrentPosition) + { + _startPoint = _endPoint; + _endPoint = startPoint.Clone(); + } + else + { + _startPoint = startPoint.Clone(); + } + } + + public override void SetEnd(TextPoint endPoint) + { + if (endPoint.TextBuffer != TextBuffer) + { + throw new ArgumentException("startPoint"); + } + + if (endPoint.CurrentPosition < _startPoint.CurrentPosition) + { + _endPoint = _startPoint; + _startPoint = endPoint.Clone(); + } + else + { + _endPoint = endPoint.Clone(); + } + } + + public override void MoveTo(TextRange newRange) + { + if (newRange.TextBuffer != TextBuffer) + { + throw new ArgumentException(Strings.OtherRangeFromWrongBuffer); + } + + _startPoint = newRange.GetStartPoint(); + _endPoint = newRange.GetEndPoint(); + } + + protected override IEnumerator<TextPoint> GetEnumeratorInternal() + { + for (int position = _startPoint.CurrentPosition; position <= _endPoint.CurrentPosition; position++) + { + TextPoint enumeratedPoint = _startPoint.Clone(); + enumeratedPoint.MoveTo(position); + + yield return enumeratedPoint; + } + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs b/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs new file mode 100644 index 0000000..80715de --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultTextViewPrimitive.cs @@ -0,0 +1,157 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Formatting; + + internal sealed class DefaultTextViewPrimitive : TextView + { + private ITextView _textView; + private Caret _caret; + private Selection _selection; + private TextBuffer _textBuffer; + private IViewPrimitivesFactoryService _viewPrimitivesFactory; + + internal DefaultTextViewPrimitive(ITextView textView, IViewPrimitivesFactoryService viewPrimitivesFactory, IBufferPrimitivesFactoryService bufferPrimitivesFactory) + { + _textView = textView; + + _viewPrimitivesFactory = viewPrimitivesFactory; + + _textBuffer = bufferPrimitivesFactory.CreateTextBuffer(textView.TextBuffer); + + _caret = _viewPrimitivesFactory.CreateCaret(this); + _selection = _viewPrimitivesFactory.CreateSelection(this); + } + + public override void MoveLineToTop(int lineNumber) + { + ITextSnapshotLine line = _textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber); + _textView.DisplayTextLineContainingBufferPosition(line.Start, 0.0, ViewRelativePosition.Top); + } + + public override void MoveLineToBottom(int lineNumber) + { + ITextSnapshotLine line = _textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(lineNumber); + _textView.DisplayTextLineContainingBufferPosition(line.Start, 0.0, ViewRelativePosition.Bottom); + } + + public override void ScrollUp(int lines) + { + _textView.ViewScroller.ScrollViewportVerticallyByPixels(((double)lines) * _textView.LineHeight); + } + + public override void ScrollDown(int lines) + { + _textView.ViewScroller.ScrollViewportVerticallyByPixels(- ((double)lines) * _textView.LineHeight); + } + + public override void ScrollPageDown() + { + _textView.ViewScroller.ScrollViewportVerticallyByPage(ScrollDirection.Down); + } + + public override void ScrollPageUp() + { + _textView.ViewScroller.ScrollViewportVerticallyByPage(ScrollDirection.Up); + } + + public override bool Show(DisplayTextPoint point, HowToShow howToShow) + { + if (howToShow == HowToShow.AsIs) + { + _textView.ViewScroller.EnsureSpanVisible(new SnapshotSpan(point.AdvancedTextPoint, 0), EnsureSpanVisibleOptions.MinimumScroll); + } + else if (howToShow == HowToShow.Centered) + { + _textView.ViewScroller.EnsureSpanVisible(new SnapshotSpan(point.AdvancedTextPoint, 0), EnsureSpanVisibleOptions.AlwaysCenter); + } + else if (howToShow == HowToShow.OnFirstLineOfView) + { + _textView.DisplayTextLineContainingBufferPosition(point.AdvancedTextPoint, 0.0, ViewRelativePosition.Top); + } + return point.IsVisible; + } + + public override VisibilityState Show(DisplayTextRange textRange, HowToShow howToShow) + { + if (howToShow == HowToShow.AsIs) + { + _textView.ViewScroller.EnsureSpanVisible(textRange.AdvancedTextRange, EnsureSpanVisibleOptions.MinimumScroll); + } + else if (howToShow == HowToShow.Centered) + { + _textView.ViewScroller.EnsureSpanVisible(textRange.AdvancedTextRange, EnsureSpanVisibleOptions.AlwaysCenter); + } + else if (howToShow == HowToShow.OnFirstLineOfView) + { + _textView.DisplayTextLineContainingBufferPosition(textRange.AdvancedTextRange.Start, 0.0, ViewRelativePosition.Top); + } + + return textRange.Visibility; + } + + public override DisplayTextPoint GetTextPoint(int position) + { + return _viewPrimitivesFactory.CreateDisplayTextPoint(this, position); + } + + public override DisplayTextPoint GetTextPoint(TextPoint textPoint) + { + return GetTextPoint(textPoint.CurrentPosition); + } + + public override DisplayTextPoint GetTextPoint(int line, int column) + { + ITextSnapshotLine snapshotLine = _textView.TextBuffer.CurrentSnapshot.GetLineFromLineNumber(line); + return GetTextPoint(snapshotLine.Start + column); + } + + public override DisplayTextRange GetTextRange(TextPoint startPoint, TextPoint endPoint) + { + return GetTextRange(TextBuffer.GetTextRange(startPoint, endPoint)); + } + + public override DisplayTextRange GetTextRange(TextRange textRange) + { + return _viewPrimitivesFactory.CreateDisplayTextRange(this, textRange); + } + + public override DisplayTextRange GetTextRange(int startPosition, int endPosition) + { + return GetTextRange(TextBuffer.GetTextPoint(startPosition), TextBuffer.GetTextPoint(endPosition)); + } + + public override DisplayTextRange VisibleSpan + { + get { return GetTextRange(_textView.TextViewLines.FirstVisibleLine.Start, _textView.TextViewLines.LastVisibleLine.EndIncludingLineBreak); } + } + + public override ITextView AdvancedTextView + { + get { return _textView; } + } + + public override Caret Caret + { + get { return _caret; } + } + + public override Selection Selection + { + get { return _selection; } + } + + public override TextBuffer TextBuffer + { + get { return _textBuffer; } + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs b/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs new file mode 100644 index 0000000..09e9d25 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/DefaultViewPrimitivesFactoryService.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System.ComponentModel.Composition; + + using Microsoft.VisualStudio.Text.Editor; + + [Export(typeof(IViewPrimitivesFactoryService))] + internal sealed class DefaultViewPrimitivesFactoryService : IViewPrimitivesFactoryService + { + [Import] + internal IEditorOptionsFactoryService EditorOptionsFactoryService { get; set; } + + [Import] + internal IBufferPrimitivesFactoryService BufferPrimitivesFactoryService { get; set; } + + #region IViewPrimitivesFactoryService Members + + public TextView CreateTextView(ITextView textView) + { + TextView textViewPrimitive = null; + + if (!textView.Properties.TryGetProperty<TextView>(EditorPrimitiveIds.ViewPrimitiveId, out textViewPrimitive)) + { + textViewPrimitive = new DefaultTextViewPrimitive(textView, this, BufferPrimitivesFactoryService); + textView.Properties.AddProperty(EditorPrimitiveIds.ViewPrimitiveId, textViewPrimitive); + } + return textViewPrimitive; + } + + public DisplayTextPoint CreateDisplayTextPoint(TextView textView, int position) + { + return new DefaultDisplayTextPointPrimitive(textView, position, EditorOptionsFactoryService.GetOptions(textView.AdvancedTextView)); + } + + public DisplayTextRange CreateDisplayTextRange(TextView textView, TextRange textRange) + { + return new DefaultDisplayTextRangePrimitive(textView, textRange); + } + + public Selection CreateSelection(TextView textView) + { + if (textView.Selection == null) + { + // The selection will add itself to the view. + return new DefaultSelectionPrimitive(textView, EditorOptionsFactoryService.GetOptions(textView.AdvancedTextView)); + } + + return textView.Selection; + } + + public Caret CreateCaret(TextView textView) + { + if (textView.Caret == null) + { + // The caret will add itself to the view. + return new DefaultCaretPrimitive(textView, EditorOptionsFactoryService.GetOptions(textView.AdvancedTextView)); + } + + return textView.Caret; + } + + #endregion + + } +} diff --git a/src/Text/Impl/EditorPrimitives/EditorPrimitivesFactoryService.cs b/src/Text/Impl/EditorPrimitives/EditorPrimitivesFactoryService.cs new file mode 100644 index 0000000..fd9be4f --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/EditorPrimitivesFactoryService.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System; + using System.ComponentModel.Composition; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + + [Export(typeof(IEditorPrimitivesFactoryService))] + internal sealed class EditorPrimitivesFactoryService : IEditorPrimitivesFactoryService + { + [Import] + internal IViewPrimitivesFactoryService ViewPrimitivesFactory { get; set; } + + [Import] + internal IBufferPrimitivesFactoryService BufferPrimitivesFactory { get; set; } + + #region IEditorPrimitivesFactoryService Members + + public IViewPrimitives GetViewPrimitives(ITextView textView) + { + return new ViewPrimitives(textView, ViewPrimitivesFactory); + } + + public IBufferPrimitives GetBufferPrimitives(ITextBuffer textBuffer) + { + return new BufferPrimitives(textBuffer, BufferPrimitivesFactory); + } + + #endregion + } +} diff --git a/src/Text/Impl/EditorPrimitives/PrimitivesUtilities.cs b/src/Text/Impl/EditorPrimitives/PrimitivesUtilities.cs new file mode 100644 index 0000000..86053b8 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/PrimitivesUtilities.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using System; + using Microsoft.VisualStudio.Text; + + /// <summary> + /// A set of utilities to help with various calculations + /// in the editor primitives. + /// </summary> + internal static class PrimitivesUtilities + { + public static int GetColumnOfPoint(ITextSnapshot textSnapshot, SnapshotPoint pointPosition, SnapshotPoint startPosition, int tabSize, Func<SnapshotPoint, SnapshotPoint> getNextPosition) + { + int columnCount = 0; + SnapshotPoint position = startPosition; + while (position < pointPosition) + { + if (textSnapshot[position] == '\t') + { + // If there is a tab in the text, then the column automatically jumps + // to the next tab stop. + columnCount = ((columnCount / tabSize) + 1) * tabSize; + } + else + { + columnCount++; + } + position = getNextPosition(position); + } + return columnCount; + } + + private static bool Edit(ITextBuffer buffer, Func<ITextEdit, bool> editAction) + { + using (ITextEdit edit = buffer.CreateEdit()) + { + if (!editAction(edit)) + return false; + + edit.Apply(); + + if (edit.Canceled) + return false; + } + + return true; + } + + public static bool Delete(ITextBuffer buffer, int start, int length) + { + return Edit(buffer, edit => edit.Delete(start, length)); + } + + internal static bool Insert(ITextBuffer buffer, int position, string text) + { + return Edit(buffer, edit => edit.Insert(position, text)); + } + + internal static bool Delete(ITextBuffer buffer, Span span) + { + return Edit(buffer, edit => edit.Delete(span)); + } + + internal static bool Delete(SnapshotSpan span) + { + return Edit(span.Snapshot.TextBuffer, edit => edit.Delete(span)); + } + + internal static bool Replace(ITextBuffer buffer, Span span, string replacement) + { + return Edit(buffer, edit => edit.Replace(span, replacement)); + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/Strings.Designer.cs b/src/Text/Impl/EditorPrimitives/Strings.Designer.cs new file mode 100644 index 0000000..20bf087 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/Strings.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:2.0.50727.1434 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.UI.Text.EditorPrimitives.Implementation.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to The other TextPoint is from a different TextBuffer than this TextPoint.. + /// </summary> + internal static string OtherPointFromWrongBuffer { + get { + return ResourceManager.GetString("OtherPointFromWrongBuffer", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The other TextRange is from a different TextBuffer than this TextRange.. + /// </summary> + internal static string OtherRangeFromWrongBuffer { + get { + return ResourceManager.GetString("OtherRangeFromWrongBuffer", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The start point's TextBuffer is different than this TextRange's TextBuffer.. + /// </summary> + internal static string StartPointFromWrongBuffer { + get { + return ResourceManager.GetString("StartPointFromWrongBuffer", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The TextPoint is from a TextBuffer that is not this one.. + /// </summary> + internal static string TextPointFromWrongBuffer { + get { + return ResourceManager.GetString("TextPointFromWrongBuffer", resourceCulture); + } + } + } +} diff --git a/src/Text/Impl/EditorPrimitives/Strings.resx b/src/Text/Impl/EditorPrimitives/Strings.resx new file mode 100644 index 0000000..ec455d7 --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/Strings.resx @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="OtherPointFromWrongBuffer" xml:space="preserve"> + <value>The other TextPoint is from a different TextBuffer than this TextPoint.</value> + </data> + <data name="OtherRangeFromWrongBuffer" xml:space="preserve"> + <value>The other TextRange is from a different TextBuffer than this TextRange.</value> + </data> + <data name="StartPointFromWrongBuffer" xml:space="preserve"> + <value>The start point's TextBuffer is different than this TextRange's TextBuffer.</value> + </data> + <data name="TextPointFromWrongBuffer" xml:space="preserve"> + <value>The TextPoint is from a TextBuffer that is not this one.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs b/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs new file mode 100644 index 0000000..9b2b1dc --- /dev/null +++ b/src/Text/Impl/EditorPrimitives/ViewPrimitives.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.EditorPrimitives.Implementation +{ + using Microsoft.VisualStudio.Text.Editor; + + internal sealed class ViewPrimitives : IViewPrimitives + { + private TextView _textView; + private Selection _selection; + private Caret _caret; + private TextBuffer _textBuffer; + + #region IViewPrimitives Members + + internal ViewPrimitives(ITextView textView, IViewPrimitivesFactoryService viewPrimitivesFactory) + { + _textView = viewPrimitivesFactory.CreateTextView(textView); + + _textBuffer = _textView.TextBuffer; + _selection = _textView.Selection; + _caret = _textView.Caret; + } + + public TextView View + { + get { return _textView; } + } + + public Selection Selection + { + get { return _selection; } + } + + public Caret Caret + { + get { return _caret; } + } + + public TextBuffer Buffer + { + get { return _textBuffer; } + } + + #endregion + } +} diff --git a/src/Text/Impl/Navigation/DefaultTextNavigator.cs b/src/Text/Impl/Navigation/DefaultTextNavigator.cs new file mode 100644 index 0000000..ccfc243 --- /dev/null +++ b/src/Text/Impl/Navigation/DefaultTextNavigator.cs @@ -0,0 +1,202 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.Diagnostics; + + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Default Text Navigation helper. + /// </summary> + internal class DefaultTextNavigator : ITextStructureNavigator + { + #region Private Members + + ITextBuffer _textBuffer; + IContentTypeRegistryService _contentTypeRegistry; + + #endregion // Private Members + + /// <summary> + /// Keep the constructor internal so that only the factory can instantiate our class. + /// </summary> + /// <param name="textBuffer"> + /// The text buffer that we will navigate on. + /// </param> + /// <param name="contentTypeRegistry"> + /// The registry for <see cref="ContentType"/>s. + /// </param> + internal DefaultTextNavigator(ITextBuffer textBuffer, IContentTypeRegistryService contentTypeRegistry) + { + // Verify + Debug.Assert(textBuffer != null); + Debug.Assert(contentTypeRegistry != null); + + _textBuffer = textBuffer; + _contentTypeRegistry = contentTypeRegistry; + } + + #region ITextStructureNavigator Members + + /// <summary> + /// Get the extent of the word at the given position. IsSignificant for the extent should be set to <c>false</c> for words + /// consisting of whitespace, unless the whitespace is a significant part of the document. If the + /// returned extent is insignificant whitespace, it should include all of the adjacent whitespace, + /// including newline characters, spaces, and tabs. + /// </summary> + /// <param name="currentPosition"> + /// The text position anywhere in the word whose extents are needed. + /// </param> + /// <returns> + /// A <see cref="TextExtent" /> describing the word. IsSignificant will be set to false for whitespace or other + /// insignificant 'words' that should be ignored during navigation. + /// </returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="currentPosition"/>is less than 0 or greater than the length of the text.</exception> + public TextExtent GetExtentOfWord(SnapshotPoint currentPosition) + { + if (currentPosition.Snapshot.TextBuffer != _textBuffer) + { + throw new ArgumentException("currentPosition TextBuffer does not match to the current TextBuffer"); + } + + if (currentPosition.Position >= currentPosition.Snapshot.Length - 1) + { + // End of document + return new TextExtent(new SnapshotSpan(currentPosition, + currentPosition.Snapshot.Length - currentPosition), + true); + } + else + { + return new TextExtent(new SnapshotSpan(currentPosition, 1), true); + } + } + + /// <summary> + /// Get the span of the enclosing syntactic element given the currently active span. + /// </summary> + /// <param name="activeSpan"> + /// The active span from where to get the span of the enclosing syntactic element. + /// </param> + /// <returns> + /// A <see cref="SnapshotSpan"/> describing the enclosing syntactic element. If the given active + /// span covers multiple syntactic elements, then the least common ancestor of the elements + /// will be returned. If it already covers the root element of the document (a.k.a the whole document), + /// then a <see cref="SnapshotSpan"/> of the same span will be returned. + /// </returns> + public SnapshotSpan GetSpanOfEnclosing(SnapshotSpan activeSpan) + { + if (activeSpan.IsEmpty && (activeSpan.Start != activeSpan.Snapshot.Length)) + { + return new SnapshotSpan(activeSpan.Start, 1); + } + return new SnapshotSpan(activeSpan.Snapshot, 0, activeSpan.Snapshot.Length); + } + + /// <summary> + /// Get the span of the first child syntactic element given the currently active span. + /// If the active span has zero length, then the default behavior would be the same to + /// GetExtentOfEnclosingParent. + /// </summary> + /// <param name="activeSpan"> + /// The active span from where to get the span of the first child syntactic element. + /// </param> + /// <returns> + /// A <see cref="SnapshotSpan" /> describing the first child syntactic element. If the given active + /// span covers multiple syntactic elements, then the span of the least common ancestor of + /// the elements will be returned. If it already covers the leaf level element of the document, + /// then a <see cref="SnapshotSpan" /> of the same span will be returned. + /// (such that, when the same size span returned, we will try get the extent of its enclosing + /// parent, which, for the third case above, will be the whole document or the whole syntactic + /// element depend on where the span lies). + /// </returns> + public SnapshotSpan GetSpanOfFirstChild(SnapshotSpan activeSpan) + { + if (activeSpan.IsEmpty) + { + return this.GetSpanOfEnclosing(activeSpan); + } + if (activeSpan.Length > 0 && activeSpan.Length < activeSpan.Snapshot.Length) + { + return new SnapshotSpan(activeSpan.Snapshot, 0, activeSpan.Snapshot.Length); + } + return new SnapshotSpan(activeSpan.Snapshot, 0, 1); + } + + /// <summary> + /// Get the span of the next sibling syntactic element given the currently active span. If the + /// active span has zero length, then the default behavior would be the same to + /// GetExtentOfEnclosingParent. + /// </summary> + /// <param name="activeSpan"> + /// The active span from where to get the span of the next sibling syntactic element. + /// </param> + /// <returns> + /// A <see cref="SnapshotSpan"/> describing the next sibling syntactic element. If the given active + /// span covers multiple syntactic elements, then the span of the next sibling element will be + /// returned. If text covered by the span doesn't followed by a sibling, then the default + /// behavior would be the same to GetExtentOfEnclosingParent. + /// </returns> + public SnapshotSpan GetSpanOfNextSibling(SnapshotSpan activeSpan) + { + if (activeSpan.IsEmpty) + { + return this.GetSpanOfEnclosing(activeSpan); + } + if (activeSpan.End == activeSpan.Snapshot.Length) + { + return new SnapshotSpan(activeSpan.Snapshot, 0, activeSpan.Snapshot.Length); + } + + return new SnapshotSpan(activeSpan.End, 1); + } + + /// <summary> + /// Get the span of the previous sibling syntactic element given the currently active span. + /// If the active span has zero length, then the default behavior would be the same to + /// GetExtentOfEnclosingParent. + /// </summary> + /// <param name="activeSpan"> + /// The active span from where to get the span of the previous sibling syntactic element. + /// </param> + /// <returns> + /// A <see cref="SnapshotSpan"/> describing the next sibling syntactic element. If the given active + /// span covers multiple syntactic elements, then the span of the previous element will be + /// returned. If text covered by the span doesn't preceded by a sibling, then the default + /// behavior would be the same to GetExtentOfEnclosingParent. + /// </returns> + public SnapshotSpan GetSpanOfPreviousSibling(SnapshotSpan activeSpan) + { + if (activeSpan.IsEmpty) + { + return this.GetSpanOfEnclosing(activeSpan); + } + if (activeSpan.Start == 0) + { + return new SnapshotSpan(activeSpan.Snapshot, 0, activeSpan.Snapshot.Length); + } + + return new SnapshotSpan(activeSpan.Start - 1, 1); + } + + /// <summary> + /// The content type that this navigator supports. + /// </summary> + public IContentType ContentType + { + get + { + return _contentTypeRegistry.UnknownContentType; + } + } + + #endregion // ITextStructureNavigator Members + } +} diff --git a/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs b/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs new file mode 100644 index 0000000..e59bc8d --- /dev/null +++ b/src/Text/Impl/Navigation/TextStructureNavigatorSelectorService.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Implementation +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Provides a service to help with the Text Structure Navigation. + /// </summary> + [Export(typeof(ITextStructureNavigatorSelectorService))] + internal sealed class TextStructureNavigatorSelectorService : ITextStructureNavigatorSelectorService + { + [Import] + internal IContentTypeRegistryService _contentTypeRegistryService { get; set; } + + [Import] + internal GuardedOperations _guardedOperations { get; set; } + + [ImportMany(typeof(ITextStructureNavigatorProvider))] + internal List<Lazy<ITextStructureNavigatorProvider, IContentTypeMetadata>> _textStructureNavigatorProviders { get; set; } + + public ITextStructureNavigator GetTextStructureNavigator(ITextBuffer textBuffer) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + + ITextStructureNavigator navigator = null; + + if (textBuffer.Properties.TryGetProperty(typeof(ITextStructureNavigator), out navigator)) + { + return navigator; + } + + navigator = CreateNavigator(textBuffer, textBuffer.ContentType); + + // Cache navigator until buffer's content type changes. + textBuffer.Properties[typeof(ITextStructureNavigator)] = navigator; + textBuffer.ContentTypeChanged += OnContentTypeChanged; + + return navigator; + } + + public ITextStructureNavigator CreateTextStructureNavigator(ITextBuffer textBuffer, IContentType contentType) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + if (contentType == null) + { + throw new ArgumentNullException("contentType"); + } + return CreateNavigator(textBuffer, contentType); + } + + #region Private Helpers + + private ITextStructureNavigator CreateNavigator(ITextBuffer textBuffer, IContentType contentType) + { + ITextStructureNavigator navigator = + _guardedOperations.InvokeBestMatchingFactory + (_textStructureNavigatorProviders, + contentType, + (provider) => (provider.CreateTextStructureNavigator(textBuffer)), + _contentTypeRegistryService, this); + + // If we're here, and there's no navigator found, we'll create a default one + if (navigator == null) + { + navigator = new DefaultTextNavigator(textBuffer, _contentTypeRegistryService); + } + + return navigator; + } + + /// <summary> + /// Invalidate our cached navigator. + /// </summary> + void OnContentTypeChanged(object sender, ContentTypeChangedEventArgs e) + { + ITextBuffer buffer = e.Before.TextBuffer; + buffer.Properties.RemoveProperty(typeof(ITextStructureNavigator)); + buffer.ContentTypeChanged -= OnContentTypeChanged; + } + #endregion // Private Helpers + } +} diff --git a/src/Text/Impl/Outlining/Collapsible.cs b/src/Text/Impl/Outlining/Collapsible.cs new file mode 100644 index 0000000..5105a72 --- /dev/null +++ b/src/Text/Impl/Outlining/Collapsible.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Outlining +{ + using System.Collections.Generic; + using System.Linq; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Text.Utilities; + using System; + + /// <summary> + /// An un-collapsed region (also the base for collapsed regions). + /// </summary> + internal class Collapsible : ICollapsible + { + public ITrackingSpan Extent { get; private set; } + + public bool IsCollapsed { get; internal set; } + + public object CollapsedForm + { + get { return ((Tag != null) ? Tag.CollapsedForm : null); } + } + + public object CollapsedHintForm + { + get { return ((Tag != null) ? Tag.CollapsedHintForm : null); } + } + + public IOutliningRegionTag Tag { get; internal set; } + + public bool IsCollapsible + { + get + { + return true; + } + } + + public override bool Equals(object obj) + { + Collapsible other = obj as Collapsible; + if (other != null) + { + return Tag.Equals(other.Tag) && + Extent.Equals(other.Extent) && + IsCollapsed == other.IsCollapsed; + } + + return false; + } + + public override int GetHashCode() + { + return Tag.GetHashCode() ^ Extent.GetHashCode() ^ IsCollapsed.GetHashCode(); + } + + + public Collapsible(ITrackingSpan underlyingSpan, IOutliningRegionTag tag) + { + Extent = underlyingSpan; + Tag = tag; + + IsCollapsed = false; + } + } + + /// <summary> + /// A collapsed region. + /// </summary> + internal class Collapsed : Collapsible, ICollapsed + { + public TrackingSpanNode<Collapsed> Node { get; set; } + + public bool IsValid { get { return IsCollapsed; } } + + public IEnumerable<ICollapsed> CollapsedChildren + { + get + { + if (!IsValid) + throw new InvalidOperationException("This collapsed region is no longer valid, as it has been expanded already."); + + return Node.Children.Select(child => child.Item); + } + } + + public void Invalidate() + { + this.IsCollapsed = false; + this.Node = null; + } + + public Collapsed(ITrackingSpan extent, IOutliningRegionTag tag) + : base(extent, tag) + { + IsCollapsed = true; + } + } +} diff --git a/src/Text/Impl/Outlining/Outlining.cd b/src/Text/Impl/Outlining/Outlining.cd new file mode 100644 index 0000000..77173b5 --- /dev/null +++ b/src/Text/Impl/Outlining/Outlining.cd @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1"> + <Class Name="Microsoft.VisualStudio.Text.Outlining.Collapsible"> + <Position X="2.25" Y="0.75" Width="2" /> + <TypeIdentifier> + <HashCode>AwAAgAAIQAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAACAg=</HashCode> + <FileName>Out.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Microsoft.VisualStudio.Text.Outlining.OutlineManager"> + <Position X="5.75" Y="0.75" Width="2.5" /> + <TypeIdentifier> + <HashCode>AQAAgAQAAAAAAAABAIAAAAAAAAAAAAEAAAAAAAgAAAE=</HashCode> + <FileName>Out.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Field Name="elisionBuffer" /> + <Field Name="subjectBuffer" /> + </ShowAsAssociation> + <ShowAsCollectionAssociation> + <Field Name="regions" /> + </ShowAsCollectionAssociation> + <Lollipop Position="0.2" /> + </Class> + <Interface Name="Microsoft.VisualStudio.Text.Projection.IElisionBuffer" Collapsed="true"> + <Position X="10.25" Y="2" Width="1.5" /> + <TypeIdentifier /> + </Interface> + <Interface Name="Microsoft.VisualStudio.Text.ITextBuffer" Collapsed="true"> + <Position X="10.25" Y="0.75" Width="1.5" /> + <TypeIdentifier /> + </Interface> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram>
\ No newline at end of file diff --git a/src/Text/Impl/Outlining/OutliningManager.cs b/src/Text/Impl/Outlining/OutliningManager.cs new file mode 100644 index 0000000..ba20323 --- /dev/null +++ b/src/Text/Impl/Outlining/OutliningManager.cs @@ -0,0 +1,741 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Outlining +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Text.Tagging; + using System.Linq; + using System.Diagnostics; + using System.Threading; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Threading; + + internal sealed class OutliningManager : IAccurateOutliningManager + { + private readonly ITextBuffer2 editBuffer; + private readonly IAccurateTagAggregator<IOutliningRegionTag> tagAggregator; + private bool isEnabled = true; + internal bool isDisposed; + + // We store only the collapsed regions and generate the expanded regions on demand. + TrackingSpanTree<Collapsed> collapsedRegionTree; + + internal OutliningManager(ITextBuffer editBuffer, ITagAggregator<IOutliningRegionTag> tagAggregator, IEditorOptions options) + { + this.editBuffer = (ITextBuffer2)editBuffer; + this.tagAggregator = tagAggregator as IAccurateTagAggregator<IOutliningRegionTag>; + + bool keepTrackingCurrent = false; + if (options != null && options.IsOptionDefined("Stress Test Mode", false)) + { + keepTrackingCurrent = options.GetOptionValue<bool>("Stress Test Mode"); + } + collapsedRegionTree = new TrackingSpanTree<Collapsed>(editBuffer, keepTrackingCurrent); + + tagAggregator.BatchedTagsChanged += OutliningRegionTagsChanged; + this.editBuffer.Changed += SourceTextChanged; + } + + #region Events and event listeners + + public event EventHandler<RegionsChangedEventArgs> RegionsChanged; + public event EventHandler<RegionsExpandedEventArgs> RegionsExpanded; + public event EventHandler<RegionsCollapsedEventArgs> RegionsCollapsed; + public event EventHandler<OutliningEnabledEventArgs> OutliningEnabledChanged; + + void OutliningRegionTagsChanged(object sender, BatchedTagsChangedEventArgs e) + { + if (!isEnabled) + { + return; + } + + // Collect the spans from the various change events + UpdateAfterChange(new NormalizedSnapshotSpanCollection(e.Spans.SelectMany(s => s.GetSpans(editBuffer)))); + } + + void SourceTextChanged(object sender, TextContentChangedEventArgs e) + { + if (!isEnabled) + { + return; + } + + if (e.Changes.Count > 0) + { + UpdateAfterChange(new NormalizedSnapshotSpanCollection(e.After, e.Changes.Select(c => c.NewSpan))); + + AvoidPartialLinebreaks(e); + } + } + + private void AvoidPartialLinebreaks(TextContentChangedEventArgs args) + { + // The elision buffer and the view don't handle collapsed region + // boundaries that fall within a two-character \r\n linebreak. + // Currently the most common cause of such situations is regex + // find replace operations and this tactical fix expands affected + // collapsed regions in response to problematic replace operations + // and any other such edits. + + var oldSnapshot = args.Before; + + bool expandAll = false; + + foreach (var change in args.Changes) + { + if (change.OldLength == 0) + continue; + + if (change.OldPosition > 0 + && oldSnapshot[change.OldPosition] == '\n' + && oldSnapshot[change.OldPosition - 1] == '\r') + { + expandAll = true; + break; + } + + if (change.OldEnd > 0 + && change.OldEnd < oldSnapshot.Length + && oldSnapshot[change.OldEnd] == '\n' + && oldSnapshot[change.OldEnd - 1] == '\r') + { + expandAll = true; + break; + } + } + + if (expandAll) + { + this.ExpandAll( + new SnapshotSpan( + args.After, + Span.FromBounds( + args.Changes[0].NewPosition, + args.Changes[args.Changes.Count - 1].NewEnd)), + collapsed => true); + } + } + + void UpdateAfterChange(NormalizedSnapshotSpanCollection changedSpans) + { + // It's possible that we've been informed of an update (via BatchedTagsChanged or otherwise) that no longer maps to the + // edit buffer. As a result of this, there aren't any changed spans for us to consider, so we can return immediately. + if (changedSpans.Count == 0) + return; + + var currentCollapsed = GetCollapsedRegionsInternal(changedSpans, exposedRegionsOnly: false); + + if (currentCollapsed.Count > 0) + { + // When getting tags, we'll try to be as minimal as possible, since + // this edit could be large and/or multi-part. We'll only examine + // the intersection of the given changed spans and the collapsed + // regions. + // NOTE: We could try to be even smarter and only use the child-most regions + // that we've collected, but it's a bit hard to determine which ones they + // are at this point (they are the nodes that have 0 children that intersect + // the changed spans, not just nodes that have 0 children). + + var snapshot = changedSpans[0].Snapshot; + var spansToCheck = NormalizedSnapshotSpanCollection.Intersection( + changedSpans, + new NormalizedSnapshotSpanCollection( + currentCollapsed.Select(c => c.Extent.GetSpan(snapshot)))); + + var newCollapsibles = CollapsiblesFromTags(tagAggregator.GetTags(spansToCheck)).Keys; + + IEnumerable<ICollapsed> removed; + + MergeRegions(currentCollapsed, newCollapsibles, out removed); + + List<ICollapsible> expandedRegions = new List<ICollapsible>(); + + foreach (var removedRegion in removed) + { + var expandedRegion = this.ExpandInternal(removedRegion); + + expandedRegions.Add(expandedRegion); + } + + if (expandedRegions.Count > 0) + { + // Send out the regions expanded event with the flag informing + // listeners that these regions are being removed. + var expandedEvent = RegionsExpanded; + if (expandedEvent != null) + { + expandedEvent(this, new RegionsExpandedEventArgs(expandedRegions, removalPending: true)); + } + } + } + + // Send out the general "outlining has changed" event + var handler = RegionsChanged; + if (handler != null) + { + handler(this, new RegionsChangedEventArgs(new SnapshotSpan(changedSpans[0].Start, changedSpans[changedSpans.Count - 1].End))); + } + } + + #endregion + + public ICollapsed TryCollapse(ICollapsible collapsible) + { + ICollapsed newCollapsed = CollapseInternal(collapsible); + + if (newCollapsed == null) + return newCollapsed; + + // Raise event. + var handler = RegionsCollapsed; + if (handler != null) + { + handler(this, new RegionsCollapsedEventArgs(Enumerable.Repeat(newCollapsed, 1))); + } + + return newCollapsed; + } + + private ICollapsed CollapseInternal(ICollapsible collapsible) + { + EnsureValid(); + + if (collapsible.IsCollapsed) + return null; + + Collapsed newCollapsed = new Collapsed(collapsible.Extent, collapsible.Tag); + + newCollapsed.Node = collapsedRegionTree.TryAddItem(newCollapsed, newCollapsed.Extent); + + if (newCollapsed.Node == null) + return null; + + return newCollapsed; + } + + public ICollapsible Expand(ICollapsed collapsed) + { + ICollapsible newCollapsible = ExpandInternal(collapsed); + + // Send out change event + var handler = RegionsExpanded; + if (handler != null) + { + handler(this, new RegionsExpandedEventArgs(Enumerable.Repeat(newCollapsible, 1))); + } + + return newCollapsible; + } + + private ICollapsible ExpandInternal(ICollapsed collapsed) + { + EnsureValid(); + + Collapsed internalCollapsed = collapsed as Collapsed; + if (internalCollapsed == null) + { + throw new ArgumentException("The given collapsed region was not created by this outlining manager.", + "collapsed"); + } + + if (!internalCollapsed.IsValid) + { + throw new InvalidOperationException("The collapsed region is invalid, meaning it has already been expanded."); + } + + if (!collapsedRegionTree.RemoveItem(internalCollapsed, internalCollapsed.Extent)) + { + throw new ApplicationException("Unable to remove the collapsed region from outlining manager, which means there is an internal " + + "consistency issue."); + } + + // Now that we've expanded the region, invalidate the ICollapsed so it can no longer be used. + internalCollapsed.Invalidate(); + + return new Collapsible(collapsed.Extent, collapsed.Tag); + } + + public IEnumerable<ICollapsed> CollapseAll(SnapshotSpan span, Predicate<ICollapsible> match) + { + return this.InternalCollapseAll(span, match, cancel: null); + } + + internal IEnumerable<ICollapsed> InternalCollapseAll(SnapshotSpan span, Predicate<ICollapsible> match, CancellationToken? cancel) + { + if (match == null) + throw new ArgumentNullException("match"); + + EnsureValid(span); + + List<ICollapsed> allCollapsed = new List<ICollapsed>(); + + foreach (var collapsible in this.InternalGetAllRegions(new NormalizedSnapshotSpanCollection(span), exposedRegionsOnly: false, cancel: cancel)) + { + if (!collapsible.IsCollapsed && collapsible.IsCollapsible && match(collapsible)) + { + var collapsed = this.CollapseInternal(collapsible); + + if (collapsed != null) + { + allCollapsed.Add(collapsed); + } + } + } + + if (allCollapsed.Count > 0) + { + // Send out change event + var handler = RegionsCollapsed; + if (handler != null) + { + handler(this, new RegionsCollapsedEventArgs(allCollapsed)); + } + } + + return allCollapsed; + } + + public IEnumerable<ICollapsible> ExpandAll(SnapshotSpan span, Predicate<ICollapsed> match) + { + return ExpandAllInternal(/*removalPending = */ false, span, match); + } + + public IEnumerable<ICollapsible> ExpandAllInternal(bool removalPending, SnapshotSpan span, Predicate<ICollapsed> match) + { + if (match == null) + throw new ArgumentNullException("match"); + + EnsureValid(span); + + List<ICollapsible> allExpanded = new List<ICollapsible>(); + + foreach (var collapsed in this.GetCollapsedRegions(span)) + { + if (match(collapsed)) + { + var expanded = this.ExpandInternal(collapsed); + + allExpanded.Add(expanded); + } + } + + if (allExpanded.Count > 0) + { + // Send out change event + var handler = RegionsExpanded; + if (handler != null) + { + handler(this, new RegionsExpandedEventArgs(allExpanded, removalPending)); + } + } + + return allExpanded; + } + + public bool Enabled + { + get + { + return this.isEnabled; + } + set + { + if (this.isEnabled != value) + { + // Expand all (if disabled) + ITextSnapshot snapshot = this.editBuffer.CurrentSnapshot; + SnapshotSpan snapshotSpan = new SnapshotSpan(snapshot, 0, snapshot.Length); + if (!value) + { + // Expand all regions, since we are going to remove them all + this.ExpandAllInternal(/*removalPending =*/ true, snapshotSpan, ((collapsed) => true)); + } + + // Update internal isEnabled flag after expanding all but before raising RegionsChanged event + this.isEnabled = value; + + // Raise RegionsChanged event for whole buffer (Before disable event) + EventHandler<RegionsChangedEventArgs> regionsChanged = RegionsChanged; + if (regionsChanged != null && !value) + { + regionsChanged(this, new RegionsChangedEventArgs(snapshotSpan)); + } + + // Raise OutliningEnabledChanged event + EventHandler<OutliningEnabledEventArgs> outliningEnabledChanged = OutliningEnabledChanged; + if (outliningEnabledChanged != null) + { + outliningEnabledChanged(this, new OutliningEnabledEventArgs(this.isEnabled)); + } + + // Raise RegionsChanged event for whole buffer (After enable event) + if (regionsChanged != null && value) + { + regionsChanged(this, new RegionsChangedEventArgs(snapshotSpan)); + } + } + } + } + + + #region Private helpers + + private SortedList<Collapsible, object> CollapsiblesFromTags(IEnumerable<IMappingTagSpan<IOutliningRegionTag>> tagSpans) + { + ITextSnapshot current = this.editBuffer.CurrentSnapshot; + + SortedList<Collapsible, object> collapsibles + = new SortedList<Collapsible, object>(new CollapsibleSorter(editBuffer)); + + foreach (var tagSpan in tagSpans) + { + var spans = tagSpan.Span.GetSpans(current); + + // We only accept this tag if it hasn't been split into multiple spans and if + // it hasn't had pieces cut out of it from projection. Also, refuse 0-length + // tags, as they wouldn't be hiding anything. + if (spans.Count == 1 && + spans[0].Length > 0 && + spans[0].Length == tagSpan.Span.GetSpans(tagSpan.Span.AnchorBuffer)[0].Length) + { + ITrackingSpan trackingSpan = current.CreateTrackingSpan(spans[0], SpanTrackingMode.EdgeExclusive); + var collapsible = new Collapsible(trackingSpan, tagSpan.Tag); + if (collapsibles.ContainsKey(collapsible)) + { + // TODO: Notify providers somehow. + // Or rewrite so that such things are legal. + Debug.WriteLine("IGNORING TAG " + spans[0] + " due to span conflict"); + } + else + { + collapsibles.Add(collapsible, null); + } + } + else + { + Debug.WriteLine("IGNORING TAG " + tagSpan.Span.GetSpans(editBuffer) + " because it was split or shortened by projection"); + } + } + + return collapsibles; + } + + private IEnumerable<ICollapsible> MergeRegions(IEnumerable<ICollapsed> currentCollapsed, IEnumerable<ICollapsible> newCollapsibles, + out IEnumerable<ICollapsed> removedRegions) + { + List<ICollapsed> toRemove = new List<ICollapsed>(); + + List<ICollapsed> oldRegions = new List<ICollapsed>(currentCollapsed); + List<ICollapsible> newRegions = new List<ICollapsible>(newCollapsibles); + + List<ICollapsible> merged = new List<ICollapsible>(oldRegions.Count + newRegions.Count); + + int oldIndex = 0; + int newIndex = 0; + + CollapsibleSorter sorter = new CollapsibleSorter(this.editBuffer); + + while (oldIndex < oldRegions.Count || newIndex < newRegions.Count) + { + if (oldIndex < oldRegions.Count && newIndex < newRegions.Count) + { + Collapsed oldRegion = oldRegions[oldIndex] as Collapsed; + ICollapsible newRegion = newRegions[newIndex]; + + int compareVal = sorter.Compare(oldRegion, newRegion); + + // Same region + if (compareVal == 0) + { + // might be the same region, but content could be new + oldRegion.Tag = newRegion.Tag; + merged.Add(oldRegion); + + oldIndex++; + newIndex++; + } + // old region comes first + else if (compareVal < 0) + { + toRemove.Add(oldRegion); + oldIndex++; + } + // new region comes first + else if (compareVal > 0) + { + merged.Add(newRegion); + newIndex++; + } + } + else if (oldIndex < oldRegions.Count) + { + toRemove.AddRange(oldRegions.GetRange(oldIndex, oldRegions.Count - oldIndex)); + break; + } + else if (newIndex < newRegions.Count) + { + merged.AddRange(newRegions.GetRange(newIndex, newRegions.Count - newIndex)); + break; + } + } + + removedRegions = toRemove; + + return merged; + } + + #endregion + + #region Getting collapsibles + + public IEnumerable<ICollapsed> GetCollapsedRegions(SnapshotSpan span) + { + return GetCollapsedRegionsInternal(new NormalizedSnapshotSpanCollection(span), exposedRegionsOnly: false); + } + + public IEnumerable<ICollapsed> GetCollapsedRegions(SnapshotSpan span, bool exposedRegionsOnly) + { + EnsureValid(span); + + return GetCollapsedRegionsInternal(new NormalizedSnapshotSpanCollection(span), exposedRegionsOnly); + } + + public IEnumerable<ICollapsed> GetCollapsedRegions(NormalizedSnapshotSpanCollection spans) + { + return GetCollapsedRegionsInternal(spans, exposedRegionsOnly: false); + } + + public IEnumerable<ICollapsed> GetCollapsedRegions(NormalizedSnapshotSpanCollection spans, bool exposedRegionsOnly) + { + return GetCollapsedRegionsInternal(spans, exposedRegionsOnly); + } + + internal IList<Collapsed> GetCollapsedRegionsInternal(NormalizedSnapshotSpanCollection spans, bool exposedRegionsOnly) + { + EnsureValid(spans); + + // No collapsed if disabled + if (!isEnabled) + { + return new List<Collapsed>(); + } + + if (exposedRegionsOnly) + return collapsedRegionTree.FindTopLevelNodesIntersecting(spans).Select(node => node.Item).ToList(); + else + return collapsedRegionTree.FindNodesIntersecting(spans).Select(node => node.Item).ToList(); + } + + public IEnumerable<ICollapsible> GetAllRegions(SnapshotSpan span) + { + return GetAllRegions(span, exposedRegionsOnly: false); + } + + public IEnumerable<ICollapsible> GetAllRegions(SnapshotSpan span, bool exposedRegionsOnly) + { + EnsureValid(span); + + return GetAllRegions(new NormalizedSnapshotSpanCollection(span), exposedRegionsOnly); + } + + public IEnumerable<ICollapsible> GetAllRegions(NormalizedSnapshotSpanCollection spans) + { + return GetAllRegions(spans, exposedRegionsOnly: false); + } + + public IEnumerable<ICollapsible> GetAllRegions(NormalizedSnapshotSpanCollection spans, bool exposedRegionsOnly) + { + return InternalGetAllRegions(spans, exposedRegionsOnly); + } + + internal IEnumerable<ICollapsible> InternalGetAllRegions(NormalizedSnapshotSpanCollection spans, bool exposedRegionsOnly, CancellationToken? cancel = null) + { + EnsureValid(spans); + + // No collapsibles if disabled + if (!isEnabled || spans.Count == 0) + { + return new List<Collapsible>(); + } + + ITextSnapshot snapshot = spans[0].Snapshot; + + IList<Collapsed> currentCollapsed = GetCollapsedRegionsInternal(spans, exposedRegionsOnly); + + IEnumerable<ICollapsible> newCollapsibles; + if (!exposedRegionsOnly || currentCollapsed.Count == 0) + { + newCollapsibles = CollapsiblesFromTags(this.InternalGetTags(spans, cancel)).Keys; + } + else + { + NormalizedSnapshotSpanCollection collapsedRegions = new NormalizedSnapshotSpanCollection(currentCollapsed.Select(c => c.Extent.GetSpan(snapshot))); + NormalizedSnapshotSpanCollection exposed = NormalizedSnapshotSpanCollection.Difference(spans, collapsedRegions); + + // Ensure there is an empty region on each end + SnapshotSpan first = spans[0]; + SnapshotSpan last = spans[spans.Count - 1]; + NormalizedSnapshotSpanCollection ends = new NormalizedSnapshotSpanCollection(new SnapshotSpan[] { new SnapshotSpan(first.Start, 0), new SnapshotSpan(last.End, 0) }); + exposed = NormalizedSnapshotSpanCollection.Union(exposed, ends); + + newCollapsibles = CollapsiblesFromTags(this.InternalGetTags(exposed, cancel)).Keys.Where(c => IsRegionExposed(c, snapshot)); + } + + IEnumerable<ICollapsed> removed; + + var merged = MergeRegions(currentCollapsed, newCollapsibles, out removed); + + // NOTE: IF we have misbehaved taggers, it is possible that we'll see invalid + // changes here in removed regions that are currently collapsed. We can deal + // with this by expanding as needed, but it will cause an event to be sent out, which will + // likely be unexpected and cause bugs in our clients. + + // There are a few ways we can deal with this: + + // #1: Expand/collapse regions and event + foreach (var removedRegion in removed) + { + Debug.Fail("Removing a region here means a tagger has misbehaved."); + if (removedRegion.IsCollapsed) + Expand(removedRegion); + } + + // Other options: + // #2: Return the new regions without doing anything special + // #3: Return the current collapsed + uncollapsed added regions + + return merged; + } + + private IEnumerable<IMappingTagSpan<IOutliningRegionTag>> InternalGetTags(NormalizedSnapshotSpanCollection spans, CancellationToken? cancel) + { + if (cancel.HasValue) + { + return this.tagAggregator.GetAllTags(spans, cancel.Value); + } + + return this.tagAggregator.GetTags(spans); + } + + bool IsRegionExposed(ICollapsible region, ITextSnapshot current) + { + var regionSpan = region.Extent.GetSpan(current); + + // Filter out regions that don't have both end points exposed. + return !collapsedRegionTree.IsPointContainedInANode(regionSpan.Start) && + !collapsedRegionTree.IsPointContainedInANode(regionSpan.End); + } + + #endregion + + #region IAccurateOutliningManager methods + public IEnumerable<ICollapsed> CollapseAll(SnapshotSpan span, Predicate<ICollapsible> match, CancellationToken cancel) + { + return this.InternalCollapseAll(span, match, cancel: cancel); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (!this.isDisposed) + { + this.isDisposed = true; + this.editBuffer.Changed -= this.SourceTextChanged; + this.tagAggregator.BatchedTagsChanged -= this.OutliningRegionTagsChanged; + this.tagAggregator.Dispose(); + } + } + + private void EnsureValid() + { + if (this.isDisposed) + { + throw new ObjectDisposedException("OutliningManager"); + } + } + + private void EnsureValid(NormalizedSnapshotSpanCollection spans) + { + EnsureValid(); + + if (spans == null) + { + throw new ArgumentNullException("spans"); + } + + if (spans.Count == 0) + { + throw new ArgumentException("The given span collection is empty.", "spans"); + } + + if (spans[0].Snapshot.TextBuffer != this.editBuffer) + { + throw new ArgumentException("The given span collection is on an invalid buffer." + + "Spans must be generated against the view model's edit buffer", + "spans"); + } + } + + private void EnsureValid(SnapshotSpan span) + { + EnsureValid(); + + if (span.Snapshot == null) + { + throw new ArgumentException("The given span is uninitialized."); + } + + if (span.Snapshot.TextBuffer != this.editBuffer) + { + throw new ArgumentException("The given span is on an invalid buffer." + + "Spans must be generated against the view model's edit buffer", + "span"); + } + } + + #endregion + } + + #region Sorter for sorted lists of collapsibles + class CollapsibleSorter : IComparer<ICollapsible> + { + private ITextBuffer SourceBuffer { get; set; } + + internal CollapsibleSorter(ITextBuffer sourceBuffer) + { + SourceBuffer = sourceBuffer; + } + + public int Compare(ICollapsible x, ICollapsible y) + { + if (x == null) + throw new ArgumentNullException("x"); + if (y == null) + throw new ArgumentNullException("y"); + + ITextSnapshot current = SourceBuffer.CurrentSnapshot; + SnapshotSpan left = x.Extent.GetSpan(current); + SnapshotSpan right = y.Extent.GetSpan(current); + + // The "first" collapsible should come first + if (left.Start != right.Start) + return left.Start.CompareTo(right.Start); + // The largest collapsible should come first + else + return -left.Length.CompareTo(right.Length); + } + } + #endregion +} diff --git a/src/Text/Impl/Outlining/OutliningManagerService.cs b/src/Text/Impl/Outlining/OutliningManagerService.cs new file mode 100644 index 0000000..5e8a795 --- /dev/null +++ b/src/Text/Impl/Outlining/OutliningManagerService.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Outlining +{ + using System; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Threading; + using Microsoft.Win32; + + [Export(typeof(IOutliningManagerService))] + internal class OutliningManagerService : IOutliningManagerService + { + [Import] + internal IBufferTagAggregatorFactoryService TagAggregatorFactory { get; set; } + + [Import] + internal IEditorOptionsFactoryService EditorOptionsFactoryService { get; set; } + + // While these are IDisposable, they are kept for the life of the textView, meaning that callers shouldn't actually + // dispose of them. + public IOutliningManager GetOutliningManager(ITextView textView) + { + if (textView == null) + throw new ArgumentNullException("textView"); + + if (!textView.Roles.Contains(PredefinedTextViewRoles.Structured)) + return null; + + return textView.Properties.GetOrCreateSingletonProperty(delegate + { + var tagAggregator = TagAggregatorFactory.CreateTagAggregator<IOutliningRegionTag>(textView.TextBuffer); + var manager = new OutliningManager(textView.TextBuffer, tagAggregator, EditorOptionsFactoryService.GlobalOptions); + textView.Closed += delegate { manager.Dispose(); }; + return manager; + }); + } + } +} diff --git a/src/Text/Impl/StandaloneUndo/AutoEnclose.cs b/src/Text/Impl/StandaloneUndo/AutoEnclose.cs new file mode 100644 index 0000000..42af7d4 --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/AutoEnclose.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; + +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + internal delegate void AutoEncloseDelegate(); + + internal class AutoEnclose : IDisposable + { + private AutoEncloseDelegate end; + + public AutoEnclose(AutoEncloseDelegate end) + { + this.end = end; + } + + public void Dispose() + { + if (end != null) end(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs b/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs new file mode 100644 index 0000000..b39446d --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/CatchOperationsFromHistoryForDelegatedPrimitive.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; + +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + /// <summary> + /// This class is to make it easy to catch new undo/redo operations while a delegated primitive + /// is in progress--it is called from DelegatedUndoPrimitive.Undo and .Redo with the IDispose + /// using pattern to set up the history to send operations our way. + /// </summary> + internal class CatchOperationsFromHistoryForDelegatedPrimitive : IDisposable + { + UndoHistoryImpl history; + DelegatedUndoPrimitiveImpl primitive; + + public CatchOperationsFromHistoryForDelegatedPrimitive(UndoHistoryImpl history, DelegatedUndoPrimitiveImpl primitive, DelegatedUndoPrimitiveState state) + { + this.history = history; + this.primitive = primitive; + + primitive.State = state; + history.ForwardToUndoOperation(primitive); + } + + public void Dispose() + { + history.EndForwardToUndoOperation(primitive); + primitive.State = DelegatedUndoPrimitiveState.Inactive; + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs new file mode 100644 index 0000000..87c83f9 --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveImpl.cs @@ -0,0 +1,128 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + /// <summary> + /// This is the implementation of a primitive to support inverse operations, where the user does not supply their own + /// primitives. Rather, the user calls "AddUndo" on the history and we build the primitive for them. + /// </summary> + internal class DelegatedUndoPrimitiveImpl : ITextUndoPrimitive + { + private Stack<UndoableOperationCurried> redoOperations; + private Stack<UndoableOperationCurried> undoOperations; + private UndoTransactionImpl parent; + private readonly UndoHistoryImpl history; + private DelegatedUndoPrimitiveState state; + + public DelegatedUndoPrimitiveState State + { + get { return state; } + set { state = value; } + } + + public DelegatedUndoPrimitiveImpl(UndoHistoryImpl history, UndoTransactionImpl parent, UndoableOperationCurried operationCurried) + { + redoOperations = new Stack<UndoableOperationCurried>(); + undoOperations = new Stack<UndoableOperationCurried>(); + + this.parent = parent; + this.history = history; + this.state = DelegatedUndoPrimitiveState.Inactive; + + undoOperations.Push(operationCurried); + } + + public bool CanRedo + { + get { return redoOperations.Count > 0; } + } + + public bool CanUndo + { + get { return undoOperations.Count > 0; } + } + + + /// <summary> + /// Here, we undo everything in the list of undo operations, and then clear the list. While this is happening, the + /// History will collect new operations for the redo list and pass them on to us. + /// </summary> + public void Undo() + { + using (new CatchOperationsFromHistoryForDelegatedPrimitive(history, this, DelegatedUndoPrimitiveState.Undoing)) + { + while (undoOperations.Count > 0) + { + undoOperations.Pop()(); + } + } + } + + /// <summary> + /// This is only called for "Redo," not for the original "Do." The action is to redo everything in the list of + /// redo operations, and then clear the list. While this is happening, the History will collect new operations + /// for the undo list and pass them on to us. + /// </summary> + public void Do() + { + using (new CatchOperationsFromHistoryForDelegatedPrimitive(history, this, DelegatedUndoPrimitiveState.Redoing)) + { + while (redoOperations.Count > 0) + { + redoOperations.Pop()(); + } + } + } + + public ITextUndoTransaction Parent + { + get { return this.parent; } + set { this.parent = value as UndoTransactionImpl; } + } + + /// <summary> + /// This is called by the UndoHistory implementation when we are mid-undo/mid-redo and + /// the history receives a new UndoableOperation. The action is then to add that operation + /// to the inverse list. + /// </summary> + /// <param name="operation"></param> + public void AddOperation(UndoableOperationCurried operation) + { + if (this.state == DelegatedUndoPrimitiveState.Redoing) + { + undoOperations.Push(operation); + } + else if (this.state == DelegatedUndoPrimitiveState.Undoing) + { + redoOperations.Push(operation); + } + else + { + throw new InvalidOperationException("Strings.DelegatedUndoPrimitiveStateDoesNotAllowAdd"); + } + } + + public bool MergeWithPreviousOnly + { + get { return true; } + } + + public bool CanMerge(ITextUndoPrimitive primitive) + { + return false; + } + + public ITextUndoPrimitive Merge(ITextUndoPrimitive primitive) + { + throw new InvalidOperationException("Strings.DelegatedUndoPrimitiveCannotMerge"); + } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveState.cs b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveState.cs new file mode 100644 index 0000000..4493465 --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/DelegatedUndoPrimitiveState.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + /// <summary> + /// These are the three states for the DelegatedUndoPrimitives. If Redoing or Undoing, a Redo or undo is in progress. In the + /// inactive case, it is illegal to send new operations to the primitive. + /// </summary> + internal enum DelegatedUndoPrimitiveState + { + /// <summary> + /// No redo or undo is in progress, and it is illegal to send new operations to the primitive. + /// </summary> + Inactive, + + /// <summary> + /// A redo is in progress. New operations go into the undo list. + /// </summary> + Redoing, + + /// <summary> + /// An undo is in progress. New operations go into the redo list. + /// </summary> + Undoing + } + +} diff --git a/src/Text/Impl/StandaloneUndo/NullMergeUndoTransactionPolicy.cs b/src/Text/Impl/StandaloneUndo/NullMergeUndoTransactionPolicy.cs new file mode 100644 index 0000000..1f897dc --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/NullMergeUndoTransactionPolicy.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; + +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + /// <summary> + /// Represents an empty <see cref="IMergeTextUndoTransactionPolicy"/> implementation, which disallows merging between transactions. + /// </summary> + public sealed class NullMergeUndoTransactionPolicy : IMergeTextUndoTransactionPolicy + { + #region Private Fields + + private static NullMergeUndoTransactionPolicy instance; + + #endregion + + #region Private Constructor + + private NullMergeUndoTransactionPolicy() { } + + #endregion + + /// <summary> + /// Gets the <see cref="NullMergeUndoTransactionPolicy"/> object. + /// </summary> + public static IMergeTextUndoTransactionPolicy Instance + { + get + { + if (NullMergeUndoTransactionPolicy.instance == null) + { + NullMergeUndoTransactionPolicy.instance = new NullMergeUndoTransactionPolicy(); + } + + return instance; + } + } + + public bool TestCompatiblePolicy(IMergeTextUndoTransactionPolicy other) + { + return false; + } + + public bool CanMerge(ITextUndoTransaction newerTransaction, ITextUndoTransaction olderTransaction) + { + return false; + } + + public void PerformTransactionMerge(ITextUndoTransaction existingTransaction, ITextUndoTransaction newTransaction) + { + throw new InvalidOperationException("Strings.NullMergePolicyCannotMerge"); + } + } +} diff --git a/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs b/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs new file mode 100644 index 0000000..9f5bac5 --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/UndoHistoryImpl.cs @@ -0,0 +1,546 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Collections.ObjectModel; +using System.ComponentModel.Composition; +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + internal class UndoHistoryImpl : ITextUndoHistory + { + public event EventHandler<TextUndoRedoEventArgs> UndoRedoHappened; + public event EventHandler<TextUndoTransactionCompletedEventArgs> UndoTransactionCompleted; + + #region Private Fields + + private UndoTransactionImpl currentTransaction; + private Stack<ITextUndoTransaction> undoStack; + private Stack<ITextUndoTransaction> redoStack; + private DelegatedUndoPrimitiveImpl activeUndoOperationPrimitive; + private TextUndoHistoryState state; + private PropertyCollection properties; + + #endregion + + internal UndoHistoryRegistryImpl UndoHistoryRegistry; + + public UndoHistoryImpl(UndoHistoryRegistryImpl undoHistoryRegistry) + { + this.currentTransaction = null; + this.UndoHistoryRegistry = undoHistoryRegistry; + this.undoStack = new Stack<ITextUndoTransaction>(); + this.redoStack = new Stack<ITextUndoTransaction>(); + this.activeUndoOperationPrimitive = null; + this.state = TextUndoHistoryState.Idle; + } + + /// <summary> + /// The full undo stack for this history. Does not include any currently opened or redo transactions. + /// </summary> + public IEnumerable<ITextUndoTransaction> UndoStack + { + get { return this.undoStack; } + } + + /// <summary> + /// The full redo stack for this history. Does not include any currently opened or undo transactions. + /// </summary> + public IEnumerable<ITextUndoTransaction> RedoStack + { + get { return this.redoStack; } + } + + /// <summary> + /// It returns most recently pushed (topmost) item of the <see cref="ITextUndoHistory.UndoStack"/> or if the stack is + /// empty it returns null. + /// </summary> + public ITextUndoTransaction LastUndoTransaction + { + get + { + if (this.undoStack.Count != 0) + { + return this.undoStack.Peek(); + } + + return null; + } + } + + /// <summary> + /// It returns most recently pushed (topmost) item of the <see cref="ITextUndoHistory.RedoStack"/> or if the stack is + /// empty it returns null. + /// </summary> + public ITextUndoTransaction LastRedoTransaction + { + get + { + if (this.redoStack.Count != 0) + { + return this.redoStack.Peek(); + } + + return null; + } + } + + /// <summary> + /// Whether a single undo is permissible (corresponds to the most recent visible undo UndoTransaction's CanUndo). + /// </summary> + /// <remarks> + /// If there are hidden transactions on top of the visible transaction, this property returns true only they are + /// undoable as well. + /// </remarks> + public bool CanUndo + { + get + { + if (this.undoStack.Count > 0) + { + return this.undoStack.Peek().CanUndo; + } + else + { + return false; + } + } + } + + /// <summary> + /// Whether a single redo is permissible (corresponds to the most recent visible redo UndoTransaction's CanRedo). + /// </summary> + /// <remarks> + /// If there are hidden transactions on top of the visible transaction, this property returns true only they are + /// redoable as well. + /// </remarks> + public bool CanRedo + { + get + { + if (this.redoStack.Count > 0) + { + return this.redoStack.Peek().CanRedo; + } + else + { + return false; + } + } + } + + /// <summary> + /// The most recent visible undo UndoTransactions's Description. + /// </summary> + public string UndoDescription + { + get + { + if (this.undoStack.Count > 0) + { + return this.undoStack.Peek().Description; + } + else + { + return "Strings.HistoryCantUndo"; + } + } + } + + /// <summary> + /// The most recent visible redo UndoTransaction's Description. + /// </summary> + public string RedoDescription + { + get + { + if (this.undoStack.Count > 0) + { + return this.redoStack.Peek().Description; + } + else + { + return "Strings.HistoryCantRedo"; + } + } + } + + /// <summary> + /// The current UndoTransaction in progress. + /// </summary> + public ITextUndoTransaction CurrentTransaction + { + get { return this.currentTransaction; } + } + + /// <summary> + /// + /// </summary> + public TextUndoHistoryState State + { + get { return this.state; } + } + + /// <summary> + /// Creates a new transaction, nests it in the previously current transaction, and marks it current. + /// If there is a redo stack, it gets cleared. + /// UNDONE: should the redo-clearing happen now or when the new transaction is committed? + /// </summary> + /// <param name="description">A string description for the transaction.</param> + /// <param name="isHidden">The new transaction.</param> + /// <returns></returns> + public ITextUndoTransaction CreateTransaction(string description) + { + if (String.IsNullOrEmpty(description)) + { + throw new ArgumentNullException("description", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "CreateTransaction", "description")); + } + + // If there is a pending transaction that has already been completed, we should not be permitted + // to open a new transaction, since it cannot later be added to its parent. + if ((this.currentTransaction != null) && (this.currentTransaction.State != UndoTransactionState.Open)) + { + throw new InvalidOperationException("Strings.CannotCreateTransactionWhenCurrentTransactionNotOpen"); + } + + // new transactions that are visible should clear the redo stack. + if (this.currentTransaction == null) + { + foreach (UndoTransactionImpl redoTransaction in this.redoStack) + { + redoTransaction.Invalidate(); + } + + this.redoStack.Clear(); + } + + UndoTransactionImpl newTransaction = new UndoTransactionImpl(this, this.currentTransaction, description); + + this.currentTransaction = newTransaction; + + return this.currentTransaction; + } + + /// <summary> + /// Performs requested amount of undo operation and places the transactions on the redo stack. + /// UNDONE: What if there is a currently opened transaction? + /// </summary> + /// <param name="count">The number of undo operations to perform. At the end of the operation, requested number of visible + /// transactions are undone. Hence actual number of transactions undone might be more than this number if there are some + /// hidden transactions adjacent to (on top of or at the bottom of) the visible ones. + /// </param> + /// <remarks> + /// After the last visible transaction is undone, hidden transactions left on top the stack are undone as well until a + /// visible or linked transaction is encountered or stack is emptied totally. + /// </remarks> + public void Undo(int count) + { + if (count <= 0) + { + throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, "Strings.RedoAndUndoAcceptOnlyPositiveCounts", "Undo", count), "count"); + } + + if (!IsThereEnoughVisibleTransactions(this.undoStack, count)) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, "Strings.CannotUndoMoreTransactionsThanExist", "undo", count)); + } + + TextUndoHistoryState originalState = this.state; + this.state = TextUndoHistoryState.Undoing; + using (new AutoEnclose(delegate { this.state = originalState; })) + { + while (count > 0) + { + if (!this.undoStack.Peek().CanUndo) + { + throw new InvalidOperationException("Strings.CannotUndoRequestedPrimitiveFromHistoryUndo"); + } + + ITextUndoTransaction ut = this.undoStack.Pop(); + ut.Undo(); + this.redoStack.Push(ut); + + RaiseUndoRedoHappened(this.state, ut); + + --count; + } + } + } + + /// <summary> + /// Performs an undo operation and places the primitives on the redo stack, up until (and + /// including) the transaction indicated. This is called by the linked undo transaction that + /// is aware of the linking relationship between transactions, and it does not call back into + /// the transactions' public Undo(). + /// </summary> + /// <param name="transaction"></param> + public void UndoInIsolation(UndoTransactionImpl transaction) + { + TextUndoHistoryState originalState = this.state; + this.state = TextUndoHistoryState.Undoing; + using (new AutoEnclose(delegate { this.state = originalState; })) + { + + if (this.undoStack.Contains(transaction)) + { + UndoTransactionImpl undone = null; + while (undone != transaction) + { + UndoTransactionImpl ut = this.undoStack.Pop() as UndoTransactionImpl; + ut.Undo(); + this.redoStack.Push(ut); + + RaiseUndoRedoHappened(this.state, ut); + + undone = ut; + } + } + } + } + + /// <summary> + /// Performs requested amount of redo operation and places the transactions on the undo stack. + /// UNDONE: What if there is a currently opened transaction? + /// </summary> + /// <param name="count">The number of redo operations to perform. At the end of the operation, requested number of visible + /// transactions are redone. Hence actual number of transactions redone might be more than this number if there are some + /// hidden transactions adjacent to (on top of or at the bottom of) the visible ones. + /// </param> + /// <remarks> + /// After the last visible transaction is redone, hidden transactions left on top the stack are redone as well until a + /// visible or linked transaction is encountered or stack is emptied totally. + /// </remarks> + public void Redo(int count) + { + if (count <= 0) + { + throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture, "Strings.RedoAndUndoAcceptOnlyPositiveCounts", "Redo", count), "count"); + } + + if (!IsThereEnoughVisibleTransactions(this.redoStack, count)) + { + throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, "Strings.CannotUndoMoreTransactionsThanExist", "redo", count)); + } + + TextUndoHistoryState originalState = this.state; + this.state = TextUndoHistoryState.Redoing; + using (new AutoEnclose(delegate { this.state = originalState; })) + { + while (count > 0) + { + if (!this.redoStack.Peek().CanRedo) + { + throw new InvalidOperationException("Strings.CannotRedoRequestedPrimitiveFromHistoryRedo"); + } + ITextUndoTransaction ut = this.redoStack.Pop(); + ut.Do(); + this.undoStack.Push(ut); + + RaiseUndoRedoHappened(this.state, ut); + + --count; + } + } + } + + /// <summary> + /// Performs a redo operation and places the primitives on the redo stack, up until (and + /// including) the transaction indicated. This is called by the linked undo transaction that + /// is aware of the linking relationship between transactions, and it does not call back into + /// the transactions' public Redo(). + /// </summary> + /// <param name="transaction"></param> + public void RedoInIsolation(UndoTransactionImpl transaction) + { + TextUndoHistoryState originalState = this.state; + this.state = TextUndoHistoryState.Redoing; + using (new AutoEnclose(delegate { this.state = originalState; })) + { + if (this.redoStack.Contains(transaction)) + { + UndoTransactionImpl redone = null; + while (redone != transaction) + { + UndoTransactionImpl ut = this.redoStack.Pop() as UndoTransactionImpl; + ut.Do(); + this.undoStack.Push(ut); + + RaiseUndoRedoHappened(this.state, ut); + + redone = ut; + } + } + } + } + + /// <summary> + /// This method is called from the DelegatedUndoPrimitive just as it starts a do or undo, so that this + /// history knows to forward any new UndoableOperations to the primitive. This and its pair EndForward... only manage + /// the state of the activeUndoOperationPrimitive. + /// </summary> + /// <param name="primitive">The delegated primitive to be marked active</param> + public void ForwardToUndoOperation(DelegatedUndoPrimitiveImpl primitive) + { + if (this.activeUndoOperationPrimitive != null) + { + throw new InvalidOperationException(); + } + + this.activeUndoOperationPrimitive = primitive; + } + + /// <summary> + /// This method ends the lifetime of the activeUndoOperationPrimitive and should be called after ForwardToUndoOperation. + /// </summary> + /// <param name="primitive">The previously active delegated primitive--used for sanity check.</param> + public void EndForwardToUndoOperation(DelegatedUndoPrimitiveImpl primitive) + { + if (this.activeUndoOperationPrimitive != primitive) + { + throw new InvalidOperationException(); + } + + this.activeUndoOperationPrimitive = null; + } + + /// <summary> + /// This is how the transactions alert their containing history that they have finished + /// (likely from the Dispose() method). + /// </summary> + /// <param name="transaction">This is the transaction that's finishing. It should match the history's current transaction. + /// If it does not match, then the current transaction will be discarded and an exception will be thrown.</param> + public void EndTransaction(ITextUndoTransaction transaction) + { + if (this.currentTransaction != transaction) + { + this.currentTransaction = null; + throw new InvalidOperationException("Strings.EndTransactionOutOfOrder"); + } + + // only add completed transactions to their parents (or the stack) + if (this.currentTransaction.State == UndoTransactionState.Completed) + { + if (this.currentTransaction.Parent == null) // stack bottomed out! + { + MergeOrPushToUndoStack(this.currentTransaction); + } + } + this.currentTransaction = this.currentTransaction.Parent as UndoTransactionImpl; + } + + /// <summary> + /// This does two different things, depending on the MergeUndoTransactionPolicys in question. + /// It either simply pushes the current transaction to the undo stack, OR it merges it with + /// the most recent item in the stack. + /// </summary> + private void MergeOrPushToUndoStack(UndoTransactionImpl transaction) + { + ITextUndoTransaction transactionAdded; + TextUndoTransactionCompletionResult transactionResult; + + UndoTransactionImpl utPrevious = this.undoStack.Count > 0 ? this.undoStack.Peek() as UndoTransactionImpl : null; + if (utPrevious != null && ProceedWithMerge(transaction, utPrevious)) + { + // Temporarily make utPrevious non-read-only, during merge. + utPrevious.IsReadOnly = false; + try + { + transaction.MergePolicy.PerformTransactionMerge(utPrevious, transaction); + } + finally + { + utPrevious.IsReadOnly = true; + } + + // utPrevious is already on the undo stack, so we don't need to add it; but report + // it as the added transaction in the UndoTransactionCompleted event. + transactionAdded = utPrevious; + transactionResult = TextUndoTransactionCompletionResult.TransactionMerged; + } + else + { + this.undoStack.Push(transaction); + + transactionAdded = transaction; + transactionResult = TextUndoTransactionCompletionResult.TransactionAdded; + } + RaiseUndoTransactionCompleted(transactionAdded, transactionResult); + } + + public bool ValidTransactionForMarkers(ITextUndoTransaction transaction) + { + return transaction == null // you can put a marker on the null transaction + || this.currentTransaction == transaction // you can put a marker on the currently active transaction + || (transaction.History == this && !(transaction.State == UndoTransactionState.Invalid)); + // and you can put a marker on any transaction in this history. + } + + public static bool IsThereEnoughVisibleTransactions(Stack<ITextUndoTransaction> stack, int visibleCount) + { + if (visibleCount <= 0) + { + return true; + } + + foreach (ITextUndoTransaction transaction in stack) + { + visibleCount--; + + if (visibleCount <= 0) + { + return true; + } + } + + return false; + } + + private bool ProceedWithMerge(UndoTransactionImpl transaction1, UndoTransactionImpl transaction2) + { + UndoHistoryRegistryImpl registry = UndoHistoryRegistry; + + return transaction1.MergePolicy != null + && transaction2.MergePolicy != null + && transaction1.MergePolicy.TestCompatiblePolicy(transaction2.MergePolicy) + && transaction1.MergePolicy.CanMerge(transaction1, transaction2); + } + + private void RaiseUndoRedoHappened(TextUndoHistoryState state, ITextUndoTransaction transaction) + { + EventHandler<TextUndoRedoEventArgs> undoRedoHappened = UndoRedoHappened; + if (undoRedoHappened != null) + { + undoRedoHappened(this, new TextUndoRedoEventArgs(state, transaction)); + } + } + + private void RaiseUndoTransactionCompleted(ITextUndoTransaction transaction, TextUndoTransactionCompletionResult result) + { + EventHandler<TextUndoTransactionCompletedEventArgs> undoTransactionAdded = UndoTransactionCompleted; + if (undoTransactionAdded != null) + { + undoTransactionAdded(this, new TextUndoTransactionCompletedEventArgs(transaction, result)); + } + } + + public PropertyCollection Properties + { + get + { + if (this.properties == null) + { + this.properties = new PropertyCollection(); + } + return this.properties; + } + } + } +} + diff --git a/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs b/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs new file mode 100644 index 0000000..2e4742d --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/UndoHistoryRegistryImpl.cs @@ -0,0 +1,273 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Globalization; +using System.ComponentModel.Composition; +using System.ComponentModel; + +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + [Export(typeof(ITextUndoHistoryRegistry))] + [Export(typeof(UndoHistoryRegistryImpl))] + internal class UndoHistoryRegistryImpl : ITextUndoHistoryRegistry + { + #region Private Fields + private Dictionary<ITextUndoHistory, int> histories; + private Dictionary<WeakReferenceForDictionaryKey, ITextUndoHistory> weakContextMapping; + private Dictionary<object, ITextUndoHistory> strongContextMapping; + #endregion // Private Fields + + public UndoHistoryRegistryImpl() + { + // set up the list of histories + histories = new Dictionary<ITextUndoHistory, int>(); + + // set up the mappings from contexts to histories + weakContextMapping = new Dictionary<WeakReferenceForDictionaryKey, ITextUndoHistory>(); + strongContextMapping = new Dictionary<object, ITextUndoHistory>(); + } + + /// <summary> + /// + /// </summary> + public IEnumerable<ITextUndoHistory> Histories + { + get { return histories.Keys; } + } + + /// <summary> + /// + /// </summary> + /// <param name="context"></param> + /// <returns></returns> + public ITextUndoHistory RegisterHistory(object context) + { + if (context == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RegisterHistory", "context")); + } + + return RegisterHistory(context, false); + } + + /// <summary> + /// + /// </summary> + /// <param name="context"></param> + /// <param name="keepAlive"></param> + /// <returns></returns> + public ITextUndoHistory RegisterHistory(object context, bool keepAlive) + { + if (context == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RegisterHistory", "context")); + } + + ITextUndoHistory result; + + if (strongContextMapping.ContainsKey(context)) + { + result = strongContextMapping[context]; + + if (!keepAlive) + { + strongContextMapping.Remove(context); + weakContextMapping.Add(new WeakReferenceForDictionaryKey(context), result); + } + } + else if (weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context))) + { + result = weakContextMapping[new WeakReferenceForDictionaryKey(context)]; + + if (keepAlive) + { + weakContextMapping.Remove(new WeakReferenceForDictionaryKey(context)); + strongContextMapping.Add(context, result); + } + } + else + { + result = new UndoHistoryImpl(this); + histories.Add(result, 1); + + if (keepAlive) + { + strongContextMapping.Add(context, result); + } + else + { + weakContextMapping.Add(new WeakReferenceForDictionaryKey(context), result); + } + } + + return result; + } + + /// <summary> + /// + /// </summary> + /// <param name="context"></param> + /// <returns></returns> + public ITextUndoHistory GetHistory(object context) + { + if (context == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "GetHistory", "context")); + } + + ITextUndoHistory result; + + if (strongContextMapping.ContainsKey(context)) + { + result = strongContextMapping[context]; + } + else if (weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context))) + { + result = weakContextMapping[new WeakReferenceForDictionaryKey(context)]; + } + else + { + throw new InvalidOperationException("Strings.GetHistoryCannotFindContextInRegistry"); + } + + return result; + } + + /// <summary> + /// + /// </summary> + /// <param name="context"></param> + /// <param name="history"></param> + /// <returns></returns> + public bool TryGetHistory(object context, out ITextUndoHistory history) + { + if (context == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "TryGetHistory", "context")); + } + + ITextUndoHistory result = null; + + if (strongContextMapping.ContainsKey(context)) + { + result = strongContextMapping[context]; + } + else if (weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context))) + { + result = weakContextMapping[new WeakReferenceForDictionaryKey(context)]; + } + + history = result; + return (result != null); + } + + /// <summary> + /// + /// </summary> + /// <param name="context"></param> + /// <param name="history"></param> + public void AttachHistory(object context, ITextUndoHistory history) + { + if (context == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "context")); + } + + if (history == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "history")); + } + + AttachHistory(context, history, false); + } + + /// <summary> + /// + /// </summary> + /// <param name="context"></param> + /// <param name="history"></param> + /// <param name="keepAlive"></param> + public void AttachHistory(object context, ITextUndoHistory history, bool keepAlive) + { + if (context == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "context")); + } + + if (history == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "AttachHistory", "history")); + } + + if (strongContextMapping.ContainsKey(context) || weakContextMapping.ContainsKey(new WeakReferenceForDictionaryKey(context))) + { + throw new InvalidOperationException("Strings.AttachHistoryAlreadyContainsContextInRegistry"); + } + + if (!histories.ContainsKey(history)) + { + histories.Add(history, 1); + } + else + { + ++histories[history]; + } + + if (keepAlive) + { + strongContextMapping.Add(context, history); + } + else + { + weakContextMapping.Add(new WeakReferenceForDictionaryKey(context), history); + } + } + + /// <summary> + /// + /// </summary> + /// <param name="history"></param> + public void RemoveHistory(ITextUndoHistory history) + { + if (history == null) + { + throw new ArgumentNullException("context", String.Format(CultureInfo.CurrentCulture, "Strings.ArgumentCannotBeNull", "RemoveHistory", "history")); + } + + if (!histories.ContainsKey(history)) + { + return; + } + + histories.Remove(history); + + List<object> strongToRemove = new List<object>(); + foreach (object o in strongContextMapping.Keys) + { + if (Object.ReferenceEquals(strongContextMapping[o], history)) + { + strongToRemove.Add(o); + } + } + strongToRemove.ForEach(delegate(object o) { strongContextMapping.Remove(o); }); + + List<WeakReferenceForDictionaryKey> weakToRemove = new List<WeakReferenceForDictionaryKey>(); + foreach (WeakReferenceForDictionaryKey o in weakContextMapping.Keys) + { + if (Object.ReferenceEquals(weakContextMapping[o], history)) + { + weakToRemove.Add(o); + } + } + weakToRemove.ForEach(delegate(WeakReferenceForDictionaryKey o) { weakContextMapping.Remove(o); }); + + return; + } + } +} diff --git a/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs b/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs new file mode 100644 index 0000000..aae1d06 --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/UndoTransactionImpl.cs @@ -0,0 +1,397 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Collections.ObjectModel; +using System.ComponentModel.Composition; + +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + internal class UndoTransactionImpl : ITextUndoTransaction + { + #region Private Fields + + private readonly UndoHistoryImpl history; + private readonly UndoTransactionImpl parent; + + private string description; + private UndoTransactionState state; + private List<ITextUndoPrimitive> primitives; + private IMergeTextUndoTransactionPolicy mergePolicy; + + #endregion + + public UndoTransactionImpl(ITextUndoHistory history, ITextUndoTransaction parent, string description) + { + if (history == null) + { + throw new ArgumentNullException("history", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "UndoTransactionImpl", "history")); + } + + if (String.IsNullOrEmpty(description)) + { + throw new ArgumentNullException("description", String.Format(CultureInfo.CurrentUICulture, "Strings.ArgumentCannotBeNull", "UndoTransactionImpl", "description")); + } + + this.history = history as UndoHistoryImpl; + + if (this.history == null) + { + throw new ArgumentException("Strings.InvalidHistoryInTransaction"); + } + + this.parent = parent as UndoTransactionImpl; + + if (this.parent == null && parent != null) + { + throw new ArgumentException("Strings.InvalidParentInTransaction"); + } + + this.description = description; + + this.state = UndoTransactionState.Open; + this.primitives = new List<ITextUndoPrimitive>(); + this.mergePolicy = NullMergeUndoTransactionPolicy.Instance; + this.IsReadOnly = true; + } + + /// <summary> + /// This is how you turn transaction into "Invalid" state. Use it to indicate that this transaction is retired forever, + /// such as when clearing transactions from the redo stack. + /// </summary> + internal void Invalidate() + { + this.state = UndoTransactionState.Invalid; + } + + internal bool IsInvalid + { + get { return this.state == UndoTransactionState.Invalid; } + } + + /// <summary> + /// Used by UndoHistoryImpl.cs to allow UndoPrimitives to be modified during merging. + /// </summary> + internal bool IsReadOnly { get; set; } + + /// <summary> + /// Description is the [localized] string that describes the transaction to a user. + /// </summary> + public string Description + { + get { return this.description; } + set { this.description = value; } + } + + /// <summary> + /// State is the UndoTransactionState for the UndoTransaction, as described in that type. + /// </summary> + public UndoTransactionState State + { + get { return this.state; } + } + + /// <summary> + /// History is a reference to the UndoHistory that contains this transaction. + /// </summary> + public ITextUndoHistory History + { + get { return this.history; } + } + + /// <summary> + /// UndoPrimitives allows access to the list of primitives in this transaction container, but should only be called + /// after the transaction has been completed. + /// </summary> + public IList<ITextUndoPrimitive> UndoPrimitives + { + get + { + if (this.IsReadOnly) + return this.primitives.AsReadOnly(); + else + return this.primitives; + } + } + + /// <summary> + /// Complete marks the transaction finished and eligible for Undo. + /// </summary> + public void Complete() + { + if (this.State != UndoTransactionState.Open) + { + throw new InvalidOperationException("Strings.CompleteCalledOnTransationThatIsNotOpened"); + } + + this.state = UndoTransactionState.Completed; + + // now we need to pump these primitives into the parent, if the parent exists. + FlattenPrimitivesToParent(); + } + + /// <summary> + /// This is called by the transaction when it is complete. It results in the parent getting + /// all of this transaction's undo history, so that transactions are not really recursive (they + /// exist for rollback). + /// </summary> + public void FlattenPrimitivesToParent() + { + if (this.parent != null) + { + // first, copy up each primitive. + this.parent.CopyPrimitivesFrom(this); + + // once all the primitives are in the parent, just clear them so + // no one has a chance to tweak them here, or do/undo us. + this.primitives.Clear(); + } + } + + /// <summary> + /// Copies all of the primitives from the given transaction, and appends them to the UndoPrimitives list. + /// </summary> + /// <param name="transaction">The UndoTransactionImpl to copy from.</param> + public void CopyPrimitivesFrom(UndoTransactionImpl transaction) + { + foreach (ITextUndoPrimitive p in transaction.UndoPrimitives) + { + this.AddUndo(p); + } + } + + /// <summary> + /// Cancel marks an Open transaction Canceled, and Undoes and clears any primitives that have been added. + /// </summary> + public void Cancel() + { + if (this.State != UndoTransactionState.Open) + { + throw new InvalidOperationException("Strings.CancelCalledOnTransationThatIsNotOpened"); + } + + for (int i = primitives.Count - 1; i >= 0; --i) + { + primitives[i].Undo(); + } + + this.primitives.Clear(); + this.state = UndoTransactionState.Canceled; + } + + /// <summary> + /// AddUndo adds a new primitive to the end of the list when the transaction is Open. + /// </summary> + /// <param name="undo"></param> + public void AddUndo(ITextUndoPrimitive undo) + { + if (State != UndoTransactionState.Open) + { + throw new InvalidOperationException("Strings.AddUndoCalledOnTransationThatIsNotOpened"); + } + + this.primitives.Add(undo); + undo.Parent = this; + + MergeMostRecentUndoPrimitive(); + } + + /// <summary> + /// This is called by AddUndo, so that primitives are always in a fully merged state as we go. + /// </summary> + protected void MergeMostRecentUndoPrimitive() + { + // no merging unless there are at least two items + if (primitives.Count < 2) + { + return; + } + + ITextUndoPrimitive top = primitives[primitives.Count - 1]; + + ITextUndoPrimitive victim = null; + int victimIndex = -1; + + for (int i = primitives.Count - 2; i >= 0; --i) + { + if (top.GetType() == primitives[i].GetType() && top.CanMerge(primitives[i])) + { + victim = primitives[i]; + victimIndex = i; + break; + } + } + + if (victim != null) + { + ITextUndoPrimitive newPrimitive = top.Merge(victim); + primitives.RemoveRange(primitives.Count - 1, 1); + primitives.RemoveRange(victimIndex, 1); + primitives.Add(newPrimitive); + } + } + + /// <summary> + /// + /// </summary> + public ITextUndoTransaction Parent + { + get { return parent; } + } + + /// <summary> + /// This is true iff every contained primitive is CanRedo and we are in an Undone state. + /// </summary> + public bool CanRedo + { + get + { + if (this.state == UndoTransactionState.Invalid) + { + return true; + } + + if (this.State != UndoTransactionState.Undone) + { + return false; + } + + foreach (ITextUndoPrimitive primitive in UndoPrimitives) + { + if (!primitive.CanRedo) + { + return false; + } + } + + return true; + } + } + + /// <summary> + /// This is true iff every contained primitive is CanUndo and we are in a Completed state. + /// </summary> + public bool CanUndo + { + get + { + if (this.state == UndoTransactionState.Invalid) + { + return true; + } + + if (this.State != UndoTransactionState.Completed) + { + return false; + } + + foreach (ITextUndoPrimitive primitive in UndoPrimitives) + { + if (!primitive.CanUndo) + { + return false; + } + } + + return true; + } + } + + /// <summary> + /// + /// </summary> + public void Do() + { + if (this.state == UndoTransactionState.Invalid) + { + return; + } + + if (!CanRedo) + { + throw new InvalidOperationException("Strings.DoCalledButCanRedoFalse"); + } + + this.state = UndoTransactionState.Redoing; + + for (int i = 0; i < primitives.Count; ++i) + { + primitives[i].Do(); + } + + this.state = UndoTransactionState.Completed; + } + + /// <summary> + /// This defers to the linked transaction if there is one. + /// </summary> + public void Undo() + { + if (this.state == UndoTransactionState.Invalid) + { + return; + } + + if (!CanUndo) + { + throw new InvalidOperationException("Strings.UndoCalledButCanUndoFalse"); + } + + this.state = UndoTransactionState.Undoing; + + for (int i = primitives.Count - 1; i >= 0; --i) + { + primitives[i].Undo(); + } + + this.state = UndoTransactionState.Undone; + } + + /// <summary> + /// + /// </summary> + public IMergeTextUndoTransactionPolicy MergePolicy + { + get { return this.mergePolicy; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + this.mergePolicy = value; + } + } + + /// <summary> + /// Closes a transaction and disposes it. + /// </summary> + public void Dispose() + { + GC.SuppressFinalize(this); + switch (this.State) + { + case UndoTransactionState.Open: + Cancel(); + break; + + case UndoTransactionState.Canceled: + case UndoTransactionState.Completed: + break; + + case UndoTransactionState.Redoing: + case UndoTransactionState.Undoing: + case UndoTransactionState.Undone: + throw new InvalidOperationException("Strings.ClosingAnOpenTransactionThatAppearsToBeUndoneOrUndoing"); + } + history.EndTransaction(this); + } + } +} diff --git a/src/Text/Impl/StandaloneUndo/UndoableOperationCurried.cs b/src/Text/Impl/StandaloneUndo/UndoableOperationCurried.cs new file mode 100644 index 0000000..2aba367 --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/UndoableOperationCurried.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + /// <summary> + /// This is the delegate that we ultimately call to perform the work of the + /// delegated undo operations. It contains information about all the parameter + /// objects as well as the history of origin. + /// </summary> + internal delegate void UndoableOperationCurried(); +} diff --git a/src/Text/Impl/StandaloneUndo/WeakReferenceForDictionaryKey.cs b/src/Text/Impl/StandaloneUndo/WeakReferenceForDictionaryKey.cs new file mode 100644 index 0000000..e0971d6 --- /dev/null +++ b/src/Text/Impl/StandaloneUndo/WeakReferenceForDictionaryKey.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Runtime.Serialization; + +namespace Microsoft.VisualStudio.Text.Operations.Standalone +{ + /// <summary> + /// This class is the same as WeakReference, except for its implementation of Object.GetHashCode() and Object.Equals(). + /// In both of those cases, it makes a best attempt at emulating the behavior of the target object, so that when + /// WeakReferences are placed within a dictionary, as key values, they are properly recognized as references to objects + /// that are the different (or the same). + /// </summary> + [Serializable] + public class WeakReferenceForDictionaryKey : WeakReference + { + private readonly int hashCode; + private const int xorWeakReference = 0xCCCC; + + public WeakReferenceForDictionaryKey(object target) + : base(target) + { + if (target != null) + hashCode = target.GetHashCode() ^ xorWeakReference; + } + + protected WeakReferenceForDictionaryKey(SerializationInfo info, StreamingContext context) : base(info, context) + { + return; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + return ; + } + + /// <summary> + /// This hash code is informed by (but not the same as) the target object, and is constant for the life + /// of the WeakReference. + /// </summary> + /// <returns>A hash code for the current Object.</returns> + public override int GetHashCode() + { + return hashCode; + } + + /// <summary> + /// Equality is trickier than the Hashcode, because of the possibility that the target of either WeakReference + /// has been GC'ed or finalized. In those cases, when it is not known that the references must have refered to + /// the same object, Equals() returns false. Otherwise, it returns the same as the underlying Equals in the + /// target objects. + /// </summary> + /// <param name="obj">The Object to compare with the current Object</param> + /// <returns>true if the specified Object is equal to the current Object; otherwise false.</returns> + public override bool Equals(object obj) + { + bool result = false; + WeakReferenceForDictionaryKey other = obj as WeakReferenceForDictionaryKey; + + if (Object.ReferenceEquals(other, null)) + { + result = false; + } + else if (Object.ReferenceEquals(this, other)) + { + result = true; + } + else + { + object thisObj = null; + object otherObj = null; + + try + { + thisObj = this.Target; + otherObj = other.Target; + } + catch (InvalidOperationException) + { + } + + if (thisObj == null || otherObj == null) + { + // these objects either refered to null (is that even possible?) or refer to + // something that has been GC'ed/finalized. the latter case is the important + // scenario for us, and we'll return false, in effect causing each GC'ed + // weak reference to appear to be equal only to itself. + result = false; + } + else + { + result = Object.Equals(thisObj, otherObj); + } + } + + return result; + } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TagAggregator/IViewTaggerMetadata.cs b/src/Text/Impl/TagAggregator/IViewTaggerMetadata.cs new file mode 100644 index 0000000..910cc91 --- /dev/null +++ b/src/Text/Impl/TagAggregator/IViewTaggerMetadata.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Tagging +{ + using System.Collections.Generic; + using System.ComponentModel; + + /// <summary> + /// The metadata interface for exporters and importers of metadata on <see cref="IViewTaggerProvider"/> factories. + /// </summary> + public interface IViewTaggerMetadata : INamedTaggerMetadata + { + /// <summary> + /// Text view roles to which the tagger provider applies. Default value of null is provided for backward + /// compatibility. + /// </summary> + [DefaultValue(null)] + IEnumerable<string> TextViewRoles { get; } + } +} diff --git a/src/Text/Impl/TagAggregator/TagAggregator.cs b/src/Text/Impl/TagAggregator/TagAggregator.cs new file mode 100644 index 0000000..2f84741 --- /dev/null +++ b/src/Text/Impl/TagAggregator/TagAggregator.cs @@ -0,0 +1,708 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Tagging.Implementation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Text.Tagging; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Threading; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// A tag aggregator gathers, projects, and aggregates tags over a given buffer graph. Consumers + /// of tags will get a TagAggregator for a view (likely) or a specific buffer (less likely) and + /// query for tags over this aggregator. When finished, consumers are expected to Dispose the + /// aggregator, so it can clean up any taggers that are disposable and any cached state it may + /// have. + /// </summary> + internal sealed class TagAggregator<T> : IAccurateTagAggregator<T> where T : ITag + { + internal TagAggregatorFactoryService TagAggregatorFactoryService { get; private set; } + internal IDictionary<ITextBuffer, IList<ITagger<T>>> taggers; + private readonly TagAggregatorOptions options; + private List<Tuple<ITagger<T>, int>> uniqueTaggers; + internal ITextView textView; // can be null + internal JoinableTaskHelper joinableTaskHelper; + + internal MappingSpanLink acculumatedSpanLinks = null; + + internal bool disposed; + internal bool initialized; + + + public TagAggregator(TagAggregatorFactoryService factory, ITextView textView, IBufferGraph bufferGraph, TagAggregatorOptions options) + { + this.TagAggregatorFactoryService = factory; + this.textView = textView; + this.BufferGraph = bufferGraph; + this.options = options; + this.joinableTaskHelper = new JoinableTaskHelper(factory.JoinableTaskContext); + + if (textView != null) + { + textView.Closed += this.OnTextView_Closed; + } + + taggers = new Dictionary<ITextBuffer, IList<ITagger<T>>>(); + uniqueTaggers = new List<Tuple<ITagger<T>, int>>(); + + if (((TagAggregatorOptions2)options).HasFlag(TagAggregatorOptions2.DeferTaggerCreation)) + { + this.joinableTaskHelper.RunOnUIThread((Action)(this.EnsureInitialized)); + } + else + { + this.Initialize(); + } + + this.BufferGraph.GraphBufferContentTypeChanged += new EventHandler<GraphBufferContentTypeChangedEventArgs>(BufferGraph_GraphBufferContentTypeChanged); + this.BufferGraph.GraphBuffersChanged += new EventHandler<GraphBuffersChangedEventArgs>(BufferGraph_GraphBuffersChanged); + } + + private void Initialize() + { + if (((TagAggregatorOptions2)this.options).HasFlag(TagAggregatorOptions2.NoProjection)) + { + this.taggers[this.BufferGraph.TopBuffer] = GatherTaggers(this.BufferGraph.TopBuffer); + } + else + { + //Construct our initial list of taggers by getting taggers for every textBuffer in the graph + this.BufferGraph.GetTextBuffers(delegate (ITextBuffer buffer) + { + this.taggers[buffer] = GatherTaggers(buffer); + return false; + }); + } + + this.initialized = true; + } + + private void EnsureInitialized() + { + if (!(this.disposed || this.initialized)) + { + this.Initialize(); + + //Raise the tags changed event over the entire buffer since we didn't give the correct results + //to anyone who might have called GetTags() before. + ITextSnapshot snapshot = this.BufferGraph.TopBuffer.CurrentSnapshot; + IMappingSpan span = this.BufferGraph.CreateMappingSpan(new SnapshotSpan(snapshot, 0, snapshot.Length), SpanTrackingMode.EdgeInclusive); + + this.RaiseEvents(this, span); + } + } + + #region ITagAggregator<T> Members + + public IBufferGraph BufferGraph { get; private set; } + + public IEnumerable<IMappingTagSpan<T>> GetTags(SnapshotSpan span) + { + if (this.disposed) + throw new ObjectDisposedException("TagAggregator"); + + if (this.uniqueTaggers.Count == 0) + { + return Enumerable.Empty<IMappingTagSpan<T>>(); + } + else + { + return InternalGetTags(new NormalizedSnapshotSpanCollection(span), cancel: null); + } + } + + public IEnumerable<IMappingTagSpan<T>> GetTags(IMappingSpan span) + { + if (span == null) + throw new ArgumentNullException("span"); + + if (this.disposed) + throw new ObjectDisposedException("TagAggregator"); + + if (this.uniqueTaggers.Count == 0) + { + return Enumerable.Empty<IMappingTagSpan<T>>(); + } + else + { + return InternalGetTags(span, cancel: null); + } + } + + public IEnumerable<IMappingTagSpan<T>> GetTags(NormalizedSnapshotSpanCollection snapshotSpans) + { + if (this.disposed) + throw new ObjectDisposedException("TagAggregator"); + + if ((this.uniqueTaggers.Count > 0) && (snapshotSpans.Count > 0)) + { + return InternalGetTags(snapshotSpans, cancel: null); + } + else + { + return Enumerable.Empty<IMappingTagSpan<T>>(); + } + } + + public event EventHandler<TagsChangedEventArgs> TagsChanged; + + public event EventHandler<BatchedTagsChangedEventArgs> BatchedTagsChanged; + + #endregion + + #region IAccurateTagAggregator<T> Members + + public IEnumerable<IMappingTagSpan<T>> GetAllTags(SnapshotSpan span, CancellationToken cancel) + { + if (this.disposed) + throw new ObjectDisposedException("TagAggregator"); + + this.EnsureInitialized(); + + if (this.uniqueTaggers.Count == 0) + { + return Enumerable.Empty<IMappingTagSpan<T>>(); + } + else + { + return InternalGetTags(new NormalizedSnapshotSpanCollection(span), cancel); + } + } + + public IEnumerable<IMappingTagSpan<T>> GetAllTags(IMappingSpan span, CancellationToken cancel) + { + if (span == null) + throw new ArgumentNullException("span"); + + if (this.disposed) + throw new ObjectDisposedException("TagAggregator"); + + this.EnsureInitialized(); + + if (this.uniqueTaggers.Count == 0) + { + return Enumerable.Empty<IMappingTagSpan<T>>(); + } + else + { + return InternalGetTags(span, cancel); + } + } + + public IEnumerable<IMappingTagSpan<T>> GetAllTags(NormalizedSnapshotSpanCollection snapshotSpans, CancellationToken cancel) + { + if (this.disposed) + throw new ObjectDisposedException("TagAggregator"); + + this.EnsureInitialized(); + + if ((this.uniqueTaggers.Count > 0) && (snapshotSpans.Count > 0)) + { + return InternalGetTags(snapshotSpans, cancel); + } + else + { + return Enumerable.Empty<IMappingTagSpan<T>>(); + } + } + #endregion + + #region IDisposable Members + public void Dispose() + { + if (this.disposed) + return; + + try + { + if (this.textView != null) + this.textView.Closed -= this.OnTextView_Closed; + + this.BufferGraph.GraphBufferContentTypeChanged -= BufferGraph_GraphBufferContentTypeChanged; + this.BufferGraph.GraphBuffersChanged -= BufferGraph_GraphBuffersChanged; + + this.DisposeAllTaggers(); + } + finally + { + this.taggers = null; + this.TagAggregatorFactoryService = null; + this.BufferGraph = null; + this.textView = null; + this.uniqueTaggers = null; + + disposed = true; + } + } + #endregion + + #region Event Handlers + /// <summary> + /// When a source tagger sends out a change event, we translate the SnapshotSpan + /// that was changed into a mapping span for our consumers. + /// </summary> + void SourceTaggerTagsChanged(object sender, SnapshotSpanEventArgs e) + { + if (this.disposed) + return; + + // Create a mapping span for the region and return that in our own event + IMappingSpan span = this.BufferGraph.CreateMappingSpan(e.Span, SpanTrackingMode.EdgeExclusive); + + RaiseEvents(sender, span); + } + + private void RaiseEvents(object sender, IMappingSpan span) + { + EventHandler<TagsChangedEventArgs> tempEvent = TagsChanged; + if (tempEvent != null) + { + this.TagAggregatorFactoryService.GuardedOperations.RaiseEvent(sender, tempEvent, new TagsChangedEventArgs(span)); + } + + if (this.BatchedTagsChanged != null) + { + var oldHead = Volatile.Read(ref this.acculumatedSpanLinks); + while (true) + { + var newHead = new MappingSpanLink(oldHead, span); + var result = Interlocked.CompareExchange(ref this.acculumatedSpanLinks, newHead, oldHead); + if (result == oldHead) + { + if (oldHead == null) + { + this.joinableTaskHelper.RunOnUIThread((Action)(this.RaiseBatchedTagsChanged)); + } + + break; + } + + oldHead = result; + } + } + } + + private void RaiseBatchedTagsChanged() + { + // We may have been disposed between when the event was + // dispatched and now; if so, just quit. + if (this.disposed) + return; + + bool raiseEvent = true; + + EventHandler<BatchedTagsChangedEventArgs> tempEvent = this.BatchedTagsChanged; + if (tempEvent != null) + { + if (this.textView != null) + { + if (this.textView.IsClosed) + { + // There's no need to actually raise the event (this probably won't happen since -- with a closed view -- there shouldn't be any listeners). + raiseEvent = false; + } + else if (this.textView.InLayout) + { + // The view is in the middle of a layout (because someone was pumping messages while handling a call from inside a layout). + // Many BatchTagsChanged handlers will not handle that situation gracefully so simply delay raising the event until + // we're no longer inside a layout. + this.joinableTaskHelper.RunOnUIThread((Action)(this.RaiseBatchedTagsChanged)); + + return; + } + } + } + else + { + raiseEvent = false; + } + + var oldHead = Volatile.Read(ref this.acculumatedSpanLinks); + while (true) + { + var result = Interlocked.CompareExchange(ref this.acculumatedSpanLinks, null, oldHead); + if (result == oldHead) + { + if (raiseEvent) + { + var spans = new List<IMappingSpan>(oldHead.Count); + do + { + spans.Add(oldHead.Span); + oldHead = oldHead.Next; + } + while (oldHead != null); + + this.TagAggregatorFactoryService.GuardedOperations.RaiseEvent(this, tempEvent, new BatchedTagsChangedEventArgs(spans)); + } + + break; + } + + oldHead = result; + } + } + + internal class MappingSpanLink + { + public readonly MappingSpanLink Next; + public readonly IMappingSpan Span; + public int Count { get { return (this.Next == null) ? 1 : (this.Next.Count + 1); } } + + public MappingSpanLink(MappingSpanLink next, IMappingSpan span) + { + this.Next = next; + this.Span = span; + } + } + + /// <summary> + /// When buffers are added or removed from the buffer graph, we (1) dispose all + /// the removed buffers' taggers (if they are disposable) and (2) collect all + /// taggers on the new buffers. + /// </summary> + void BufferGraph_GraphBuffersChanged(object sender, GraphBuffersChangedEventArgs e) + { + if (this.disposed || (!this.initialized) || (((TagAggregatorOptions2)this.options).HasFlag(TagAggregatorOptions2.NoProjection))) + return; + + foreach (ITextBuffer buffer in e.RemovedBuffers) + { + DisposeAllTaggersOverBuffer(buffer); + taggers.Remove(buffer); + } + + foreach (ITextBuffer buffer in e.AddedBuffers) + { + taggers[buffer] = GatherTaggers(buffer); + } + } + + /// <summary> + /// If the content type of any of the source buffers changes, we need to dispose + /// all the taggers on the buffer that we have cached (if they are disposable) and get + /// new ones. + /// </summary> + void BufferGraph_GraphBufferContentTypeChanged(object sender, GraphBufferContentTypeChangedEventArgs e) + { + if (this.disposed || !this.initialized || (((TagAggregatorOptions2)this.options).HasFlag(TagAggregatorOptions2.NoProjection) && (e.TextBuffer != this.BufferGraph.TopBuffer))) + return; + + DisposeAllTaggersOverBuffer(e.TextBuffer); + taggers[e.TextBuffer] = GatherTaggers(e.TextBuffer); + + // Send out an event to say that tags have changed over the entire text buffer, to + // be safe. + ITextSnapshot snapshot = e.TextBuffer.CurrentSnapshot; + SnapshotSpan entireSnapshot = new SnapshotSpan(snapshot, 0, snapshot.Length); + IMappingSpan span = this.BufferGraph.CreateMappingSpan(entireSnapshot, SpanTrackingMode.EdgeInclusive); + + this.RaiseEvents(this, span); + } + + private void OnTextView_Closed(object sender, EventArgs args) + { + this.Dispose(); + } + #endregion + + #region Helpers + private IEnumerable<IMappingTagSpan<T>> GetTagsForBuffer(KeyValuePair<ITextBuffer, IList<ITagger<T>>> bufferAndTaggers, + NormalizedSnapshotSpanCollection snapshotSpans, + ITextSnapshot root, CancellationToken? cancel) + { + ITextSnapshot snapshot = snapshotSpans[0].Snapshot; + + for (int t = 0; t < bufferAndTaggers.Value.Count; ++t) + { + ITagger<T> tagger = bufferAndTaggers.Value[t]; + IEnumerator<ITagSpan<T>> tags = null; + try + { + IEnumerable<ITagSpan<T>> tagEnumerable; + + if (cancel.HasValue) + { + cancel.Value.ThrowIfCancellationRequested(); + + var tagger2 = tagger as IAccurateTagger<T>; + if (tagger2 != null) + { + tagEnumerable = tagger2.GetAllTags(snapshotSpans, cancel.Value); + } + else + { + tagEnumerable = tagger.GetTags(snapshotSpans); + } + } + else + { + tagEnumerable = tagger.GetTags(snapshotSpans); + } + + if (tagEnumerable != null) + tags = tagEnumerable.GetEnumerator(); + } + catch (OperationCanceledException) + { + // Rethrow cancellation exceptions since we expect our callers to deal with it. + throw; + } + catch (Exception e) + { + this.TagAggregatorFactoryService.GuardedOperations.HandleException(tagger, e); + } + + if (tags != null) + { + try + { + while (true) + { + ITagSpan<T> tagSpan = null; + try + { + if (tags.MoveNext()) + tagSpan = tags.Current; + } + catch (Exception e) + { + this.TagAggregatorFactoryService.GuardedOperations.HandleException(tagger, e); + } + + if (tagSpan == null) + break; + + var snapshotSpan = tagSpan.Span; + + if (snapshotSpans.IntersectsWith(snapshotSpan.TranslateTo(snapshot, SpanTrackingMode.EdgeExclusive))) + { + yield return new MappingTagSpan<T>( + (root == null) + ? this.BufferGraph.CreateMappingSpan(snapshotSpan, SpanTrackingMode.EdgeExclusive) + : MappingSpanSnapshot.Create(root, snapshotSpan, SpanTrackingMode.EdgeExclusive, this.BufferGraph), + tagSpan.Tag); + } + else + { +#if DEBUG + Debug.WriteLine("tagger provided an extra (non-intersecting) tag at " + snapshotSpan + " when queried for tags over " + snapshotSpans); +#endif + } + } + } + finally + { + try + { + tags.Dispose(); + } + catch (Exception e) + { + this.TagAggregatorFactoryService.GuardedOperations.HandleException(tagger, e); + } + } + } + } + } + + private IEnumerable<IMappingTagSpan<T>> InternalGetTags(NormalizedSnapshotSpanCollection snapshotSpans, CancellationToken? cancel) + { + ITextSnapshot targetSnapshot = snapshotSpans[0].Snapshot; + + bool mapByContentType = (options & TagAggregatorOptions.MapByContentType) != 0; + + foreach (var bufferAndTaggers in taggers) + { + if (bufferAndTaggers.Value.Count > 0) + { + FrugalList<SnapshotSpan> targetSpans = new FrugalList<SnapshotSpan>(); + for (int s = 0; s < snapshotSpans.Count; ++s) + { + MappingHelper.MapDownToBufferNoTrack(snapshotSpans[s], bufferAndTaggers.Key, targetSpans, mapByContentType); + } + + if (targetSpans.Count > 0) + { + NormalizedSnapshotSpanCollection targetSpanCollection = + new NormalizedSnapshotSpanCollection(targetSpans); + + foreach (var tagSpan in this.GetTagsForBuffer(bufferAndTaggers, targetSpanCollection, targetSnapshot, cancel)) + { + yield return tagSpan; + } + } + } + } + } + + private IEnumerable<IMappingTagSpan<T>> InternalGetTags(IMappingSpan mappingSpan, CancellationToken? cancel) + { + foreach (var bufferAndTaggers in taggers) + { + if (bufferAndTaggers.Value.Count > 0) + { + NormalizedSnapshotSpanCollection spans = mappingSpan.GetSpans(bufferAndTaggers.Key); + + if (spans.Count > 0) + { + foreach (var tagSpan in this.GetTagsForBuffer(bufferAndTaggers, spans, null, cancel)) + { + yield return tagSpan; + } + } + } + } + } + + void DisposeAllTaggers() + { + foreach (var bufferAndTaggers in taggers) + { + DisposeAllTaggersOverBuffer(bufferAndTaggers.Value); + } + } + + void DisposeAllTaggersOverBuffer(ITextBuffer buffer) + { + DisposeAllTaggersOverBuffer(taggers[buffer]); + } + + void DisposeAllTaggersOverBuffer(IList<ITagger<T>> taggersOnBuffer) + { + foreach (ITagger<T> tagger in taggersOnBuffer) + { + this.UnregisterTagger(tagger); + } + } + + internal IList<ITagger<T>> GatherTaggers(ITextBuffer textBuffer) + { + List<ITagger<T>> newTaggers = new List<ITagger<T>>(); + + var bufferTaggerFactories = this.TagAggregatorFactoryService.GuardedOperations.FindEligibleFactories(this.TagAggregatorFactoryService.GetBufferTaggersForType(textBuffer.ContentType, typeof(T)), + textBuffer.ContentType, + this.TagAggregatorFactoryService.ContentTypeRegistryService); + + foreach (var factory in bufferTaggerFactories) + { + ITaggerProvider provider = null; + ITagger<T> tagger = null; + + try + { + provider = factory.Value; + tagger = provider.CreateTagger<T>(textBuffer); + } + catch (Exception e) + { + object errorSource = (provider != null) ? (object)provider : factory; + this.TagAggregatorFactoryService.GuardedOperations.HandleException(errorSource, e); + } + + this.RegisterTagger(tagger, newTaggers); + } + + if (this.textView != null) + { + var viewTaggerFactories = this.TagAggregatorFactoryService.GuardedOperations.FindEligibleFactories(this.TagAggregatorFactoryService.GetViewTaggersForType(textBuffer.ContentType, typeof(T)).Where(f => + (f.Metadata.TextViewRoles == null) || this.textView.Roles.ContainsAny(f.Metadata.TextViewRoles)), + textBuffer.ContentType, + this.TagAggregatorFactoryService.ContentTypeRegistryService); + + foreach (var factory in viewTaggerFactories) + { + IViewTaggerProvider provider = null; + ITagger<T> tagger = null; + + try + { + provider = factory.Value; + tagger = provider.CreateTagger<T>(this.textView, textBuffer); + } + catch (Exception e) + { + object errorSource = (provider != null) ? (object)provider : factory; + this.TagAggregatorFactoryService.GuardedOperations.HandleException(errorSource, e); + } + + this.RegisterTagger(tagger, newTaggers); + } + } + + return newTaggers; + } + + private void UnregisterTagger(ITagger<T> tagger) + { + int taggerIndex = uniqueTaggers.FindIndex((tuple) => object.ReferenceEquals(tuple.Item1, tagger)); + + if (taggerIndex != -1) + { + Tuple<ITagger<T>, int> taggerData = this.uniqueTaggers[taggerIndex]; + + // Is there only one reference remaining for this item? + if (taggerData.Item2 == 1) + { + tagger.TagsChanged -= SourceTaggerTagsChanged; + + this.uniqueTaggers.RemoveAt(taggerIndex); + } + else + { + // Decrease the ref count of the tagger by 1 + this.uniqueTaggers[taggerIndex] = Tuple.Create(tagger, taggerData.Item2 - 1); + } + } + else + { + Debug.Fail("The tagger should still be in the list of unique taggers."); + } + + IDisposable disposable = tagger as IDisposable; + if (disposable != null) + { + this.TagAggregatorFactoryService.GuardedOperations.CallExtensionPoint(this, () => disposable.Dispose()); + } + } + + private void RegisterTagger(ITagger<T> tagger, IList<ITagger<T>> newTaggers) + { + if (tagger != null) + { + newTaggers.Add(tagger); + + int taggerIndex = this.uniqueTaggers.FindIndex((tuple) => object.ReferenceEquals(tuple.Item1, tagger)); + + // Only subscribe to the event if we've never seen this tagger before + if (taggerIndex == -1) + { + tagger.TagsChanged += SourceTaggerTagsChanged; + + uniqueTaggers.Add(Tuple.Create(tagger, 1)); + } + else + { + // Increase the reference count for the existing tagger + uniqueTaggers[taggerIndex] = Tuple.Create(tagger, uniqueTaggers[taggerIndex].Item2 + 1); + } + } + } + + #endregion + } +} diff --git a/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs b/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs new file mode 100644 index 0000000..1b743a5 --- /dev/null +++ b/src/Text/Impl/TagAggregator/TagAggregatorFactoryService.cs @@ -0,0 +1,168 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Tagging.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.ComponentModel.Composition; + using System.Linq; + + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Threading; + + /// <summary> + /// Exports the TagAggregator provider, both the buffer and view version. + /// </summary> + [Export(typeof(IBufferTagAggregatorFactoryService))] + [Export(typeof(IViewTagAggregatorFactoryService))] + internal sealed class TagAggregatorFactoryService : IBufferTagAggregatorFactoryService, IViewTagAggregatorFactoryService + { + [ImportMany(typeof(ITaggerProvider))] + internal List<Lazy<ITaggerProvider, INamedTaggerMetadata>> BufferTaggerProviders { get; set; } + + [ImportMany(typeof(IViewTaggerProvider))] + internal List<Lazy<IViewTaggerProvider, IViewTaggerMetadata>> ViewTaggerProviders { get; set; } + + [Import] + internal IBufferGraphFactoryService BufferGraphFactoryService { get; set; } + + [Import] + internal IContentTypeRegistryService ContentTypeRegistryService { get; set; } + + [Import] + internal JoinableTaskContext JoinableTaskContext { get; set; } + + [Import] + internal GuardedOperations GuardedOperations { get; set; } + + internal ImmutableDictionary<ContentAndTypeData, IEnumerable<Lazy<ITaggerProvider, INamedTaggerMetadata>>> _bufferTaggerProviderMap = ImmutableDictionary<ContentAndTypeData, IEnumerable<Lazy<ITaggerProvider, INamedTaggerMetadata>>>.Empty; + internal ImmutableDictionary<ContentAndTypeData, IEnumerable<Lazy<IViewTaggerProvider, IViewTaggerMetadata>>> _viewTaggerProviderMap = ImmutableDictionary<ContentAndTypeData, IEnumerable<Lazy<IViewTaggerProvider, IViewTaggerMetadata>>>.Empty; + + #region IBufferTagAggregatorFactoryService Members + + public ITagAggregator<T> CreateTagAggregator<T>(ITextBuffer textBuffer) where T : ITag + { + return CreateTagAggregator<T>(textBuffer, TagAggregatorOptions.None); + } + + public ITagAggregator<T> CreateTagAggregator<T>(ITextBuffer textBuffer, TagAggregatorOptions options) where T : ITag + { + if (textBuffer == null) + throw new ArgumentNullException("textBuffer"); + + return new TagAggregator<T>(this, null, this.BufferGraphFactoryService.CreateBufferGraph(textBuffer), options); + + } + + #endregion + + #region IViewTagAggregatorFactoryService Members + + public ITagAggregator<T> CreateTagAggregator<T>(ITextView textView) where T : ITag + { + return CreateTagAggregator<T>(textView, TagAggregatorOptions.None); + } + + public ITagAggregator<T> CreateTagAggregator<T>(ITextView textView, TagAggregatorOptions options) where T : ITag + { + if (textView == null) + throw new ArgumentNullException("textView"); + + return new TagAggregator<T>(this, textView, textView.BufferGraph, options); + } + + #endregion + + internal IEnumerable<Lazy<ITaggerProvider, INamedTaggerMetadata>> GetBufferTaggersForType(IContentType type, Type taggerType) + { + var key = new ContentAndTypeData(type, taggerType); + + IEnumerable<Lazy<ITaggerProvider, INamedTaggerMetadata>> taggers; + if (!_bufferTaggerProviderMap.TryGetValue(key, out taggers)) + { + taggers = new List<Lazy<ITaggerProvider, INamedTaggerMetadata>>(this.BufferTaggerProviders.Where(f => Match(type, taggerType, f.Metadata))); + + ImmutableInterlocked.Update(ref _bufferTaggerProviderMap, (s) => s.Add(key, taggers)); + } + + return taggers; + } + + internal IEnumerable<Lazy<IViewTaggerProvider, IViewTaggerMetadata>> GetViewTaggersForType(IContentType type, Type taggerType) + { + var key = new ContentAndTypeData(type, taggerType); + + IEnumerable<Lazy<IViewTaggerProvider, IViewTaggerMetadata>> taggers; + if (!_viewTaggerProviderMap.TryGetValue(key, out taggers)) + { + taggers = new List<Lazy<IViewTaggerProvider, IViewTaggerMetadata>>(this.ViewTaggerProviders.Where(f => Match(type, taggerType, f.Metadata))); + + ImmutableInterlocked.Update(ref _viewTaggerProviderMap, (s) => s.Add(key, taggers)); + } + + return taggers; + } + + private static bool Match(IContentType bufferContentType, Type taggerType, INamedTaggerMetadata tagMetadata) + { + bool contentTypeMatch = false; + + foreach (string contentType in tagMetadata.ContentTypes) + { + if (bufferContentType.IsOfType(contentType)) + { + contentTypeMatch = true; + break; + } + } + + if (contentTypeMatch) + { + // Now find out if it can provide tags of the type we want + foreach (Type type in tagMetadata.TagTypes) + { + // This producer is used if it claims to produce a tag + // that this type is assignable from. + if (taggerType.IsAssignableFrom(type)) + { + return true; + } + } + } + + return false; + } + + internal class ContentAndTypeData + { + public readonly IContentType ContentType; + public readonly Type TaggerType; + + public ContentAndTypeData(IContentType contentType, Type taggerType) + { + this.ContentType = contentType; + this.TaggerType = taggerType; + } + + public override bool Equals(object obj) + { + var other = obj as ContentAndTypeData; + return (other != null) && (other.ContentType == this.ContentType) && (other.TaggerType == this.TaggerType); + } + + public override int GetHashCode() + { + return this.ContentType.GetHashCode() ^ this.TaggerType.GetHashCode(); + } + } + } +} diff --git a/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs b/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs new file mode 100644 index 0000000..75dcf7e --- /dev/null +++ b/src/Text/Impl/TextBufferUndoManager/Strings.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:2.0.50727.1426 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Logic.Text.BufferUndoManager.Implementation.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot redo this change.. + /// </summary> + internal static string CannotRedo { + get { + return ResourceManager.GetString("CannotRedo", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot undo this change.. + /// </summary> + internal static string CannotUndo { + get { + return ResourceManager.GetString("CannotUndo", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Create Read Only Region. + /// </summary> + internal static string ReadOnlyRegionCreated { + get { + return ResourceManager.GetString("ReadOnlyRegionCreated", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Remove Read Only Region. + /// </summary> + internal static string ReadOnlyRegionRemoved { + get { + return ResourceManager.GetString("ReadOnlyRegionRemoved", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Text Buffer Change. + /// </summary> + internal static string TextBufferChanged { + get { + return ResourceManager.GetString("TextBufferChanged", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to TextVersion has no associated changes. + /// </summary> + internal static string TextVersionNoChanges { + get { + return ResourceManager.GetString("TextVersionNoChanges", resourceCulture); + } + } + } +} diff --git a/src/Text/Impl/TextBufferUndoManager/Strings.resx b/src/Text/Impl/TextBufferUndoManager/Strings.resx new file mode 100644 index 0000000..cafb25d --- /dev/null +++ b/src/Text/Impl/TextBufferUndoManager/Strings.resx @@ -0,0 +1,138 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="CannotUndo" xml:space="preserve"> + <value>Cannot undo this change.</value> + </data> + <data name="CannotRedo" xml:space="preserve"> + <value>Cannot redo this change.</value> + </data> + <data name="TextBufferChanged" xml:space="preserve"> + <value>Text Buffer Change</value> + </data> + <data name="ReadOnlyRegionCreated" xml:space="preserve"> + <value>Create Read Only Region</value> + </data> + <data name="ReadOnlyRegionRemoved" xml:space="preserve"> + <value>Remove Read Only Region</value> + </data> + <data name="TextVersionNoChanges" xml:space="preserve"> + <value>TextVersion has no associated changes</value> + </data> +</root>
\ No newline at end of file diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs new file mode 100644 index 0000000..b4831e4 --- /dev/null +++ b/src/Text/Impl/TextBufferUndoManager/TextBufferChangeUndoPrimitive.cs @@ -0,0 +1,287 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation +{ + using System; + using System.Text; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Operations; + using System.Collections.Generic; + using System.Diagnostics; + + /// <summary> + /// The UndoPrimitive for a text buffer change operation. + /// </summary> + internal class TextBufferChangeUndoPrimitive : TextUndoPrimitive + { + #region Private Data Members + + private bool _canUndo; + + private readonly ITextUndoHistory _undoHistory; + private WeakReference _weakBufferReference; + + private readonly INormalizedTextChangeCollection _textChanges; + private int? _beforeVersion; + private int? _afterVersion; +#if DEBUG + private int _bufferLengthAfterChange; +#endif + + #endregion // Private Data Members + + /// <summary> + /// Constructs a TextBufferChangeUndoPrimitive. + /// </summary> + /// <param name="undoHistory"> + /// The ITextUndoHistory this change will be added to. + /// </param> + /// <param name="textVersion"> + /// The <see cref="ITextVersion" /> representing this change. + /// This is actually the version associated with the snapshot prior to the change. + /// </param> + /// <exception cref="ArgumentNullException"><paramref name="undoHistory"/> is null.</exception> + /// <exception cref="ArgumentNullException"><paramref name="textVersion"/> is null.</exception> + public TextBufferChangeUndoPrimitive(ITextUndoHistory undoHistory, ITextVersion textVersion) + { + // Verify input parameters + if (undoHistory == null) + { + throw new ArgumentNullException("undoHistory"); + } + + if (textVersion == null) + { + throw new ArgumentNullException("textVersion"); + } + + _textChanges = textVersion.Changes; + _beforeVersion = textVersion.ReiteratedVersionNumber; + _afterVersion = textVersion.Next.VersionNumber; + Debug.Assert(textVersion.Next.VersionNumber == textVersion.Next.ReiteratedVersionNumber, + "Creating a TextBufferChangeUndoPrimitive for a change that has previously been undone? This is probably wrong."); + + _undoHistory = undoHistory; + TextBuffer = textVersion.TextBuffer; + AttachedToNewBuffer = false; + + _canUndo = true; + +#if DEBUG + // for debug sanity checks + _bufferLengthAfterChange = textVersion.Next.Length; +#endif + } + + #region UndoPrimitive Members + + /// <summary> + /// Returns true if operation can be undone, false otherwise. + /// </summary> + public override bool CanUndo + { + get + { + // NOTE: We don't know for sure if we can undo (it might get blocked by a readonly region or a + // canceled edit), in which case the actual Undo() will fail. + return _canUndo; + } + } + + /// <summary> + /// Returns true if operation can be redone, false otherwise. + /// </summary> + public override bool CanRedo + { + get + { + // NOTE: We don't know for sure if we can redo (it might get blocked by a readonly region or a + // canceled edit), in which case the actual Do() will fail. + return !_canUndo; + } + } + + /// <summary> + /// Redo the text buffer change action. + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be redone.</exception> + public override void Do() + { + // Validate, we shouldn't be allowed to undo + if (!CanRedo) + { + throw new InvalidOperationException(Strings.CannotRedo); + } + + // For undo-in-closed-files scenarios where we are done/undone on a buffer other + // than the one we were originally created on. + if (AttachedToNewBuffer) + { + AttachedToNewBuffer = false; + + _beforeVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber; + _afterVersion = null; + } + + bool editCanceled = false; + using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, _afterVersion, typeof(TextBufferChangeUndoPrimitive))) + { + foreach (ITextChange textChange in _textChanges) + { + if (!edit.Replace(new Span(textChange.OldPosition, textChange.OldLength), textChange.NewText)) + { + // redo canceled by readonly region + editCanceled = true; + break; + } + } + + if (!editCanceled) + { + edit.Apply(); + + if (edit.Canceled) + { + editCanceled = true; + } + } + } + + if (editCanceled) + { + throw new OperationCanceledException("Redo failed due to readonly regions or canceled edit."); + } + + if (_afterVersion == null) + { + _afterVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber; + } + +#if DEBUG + // sanity check + Debug.Assert(TextBuffer.CurrentSnapshot.Length == _bufferLengthAfterChange, + "The buffer is in a different state than when this TextBufferChangeUndoPrimitive was created!"); +#endif + + _canUndo = true; + } + + /// <summary> + /// Undo the text buffer change action. + /// </summary> + /// <exception cref="InvalidOperationException">Operation cannot be undone.</exception> + public override void Undo() + { + // Validate that we can undo this change + if (!CanUndo) + { + throw new InvalidOperationException(Strings.CannotUndo); + } + +#if DEBUG + // sanity check + Debug.Assert(TextBuffer.CurrentSnapshot.Length == _bufferLengthAfterChange, + "The buffer is in a different state than when this TextBufferUndoChangePrimitive was created!"); +#endif + + // For undo-in-closed-files scenarios where we are done/undone on a buffer other + // than the one we were originally created on. + if (AttachedToNewBuffer) + { + AttachedToNewBuffer = false; + + _beforeVersion = null; + _afterVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber; + } + + bool editCanceled = false; + using (ITextEdit edit = TextBuffer.CreateEdit(EditOptions.None, _beforeVersion, typeof(TextBufferChangeUndoPrimitive))) + { + foreach (ITextChange textChange in _textChanges) + { + if (!edit.Replace(new Span(textChange.NewPosition, textChange.NewLength), textChange.OldText)) + { + // undo canceled by readonly region + editCanceled = true; + break; + } + } + + if (!editCanceled) + { + edit.Apply(); + + if (edit.Canceled) + { + editCanceled = true; + } + } + } + + if (editCanceled) + { + throw new OperationCanceledException("Undo failed due to readonly regions or canceled edit."); + } + + if (_beforeVersion == null) + { + _beforeVersion = TextBuffer.CurrentSnapshot.Version.VersionNumber; + } + + _canUndo = false; + } + + public override bool CanMerge(ITextUndoPrimitive older) + { + return false; + } + #endregion + + #region Private Helpers + /// <summary> + /// We track our ITextBuffer in ITextUndoHistory.Properties so that we can be redirected to act on a + /// different ITextBuffer in the undo-in-closed-files scenario. + /// </summary> + private ITextBuffer TextBuffer + { + get + { + ITextBuffer buffer; + if (!_undoHistory.Properties.TryGetProperty(typeof(ITextBuffer), out buffer)) + { + Debug.Assert(false); + throw new InvalidOperationException("ITextUndoHistory.Properties must contain an entry for the ITextBuffer this TextBufferChangeUndoPrimitive should act against."); + } + + return buffer; + } + set + { + _undoHistory.Properties[typeof(ITextBuffer)] = value; + } + } + + private bool AttachedToNewBuffer + { + get + { + return _weakBufferReference.Target != TextBuffer; + } + set + { + if (value != false) + { + throw new InvalidOperationException("AttachedToNewBuffer can only be reset to false."); + } + Debug.Assert(TextBuffer != null); + _weakBufferReference = new WeakReference(TextBuffer); + } + } + #endregion + } +} diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs new file mode 100644 index 0000000..9fe2e37 --- /dev/null +++ b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManager.cs @@ -0,0 +1,204 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation +{ + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Operations; + using System.Diagnostics; + + class TextBufferUndoManager : ITextBufferUndoManager, IDisposable + { + #region Private Members + + ITextBuffer _textBuffer; + ITextUndoHistoryRegistry _undoHistoryRegistry; + ITextUndoHistory _undoHistory; + Queue<ITextVersion> _editVersionList = new Queue<ITextVersion>(); + bool _inPostChanged; + + #endregion + + public TextBufferUndoManager(ITextBuffer textBuffer, ITextUndoHistoryRegistry undoHistoryRegistry) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + + if (undoHistoryRegistry == null) + { + throw new ArgumentNullException("undoHistoryRegistry"); + } + + _textBuffer = textBuffer; + + _undoHistoryRegistry = undoHistoryRegistry; + + // Register the undo history + _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer); + + // Listen for the buffer changed events so that we can make them undo/redo-able + _textBuffer.Changed += TextBufferChanged; + _textBuffer.PostChanged += TextBufferPostChanged; + _textBuffer.Changing += TextBufferChanging; + } + + #region Private Methods + + private void TextBufferChanged(object sender, TextContentChangedEventArgs e) + { + Debug.Assert((e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive) || + (_undoHistory.State != TextUndoHistoryState.Idle), + "We are undoing/redoing a change while UndoHistory.State is Idle. Something is wrong with the state."); + + // If this change didn't originate from undo, add a TextBufferChangeUndoPrimitive to our history. + if (_undoHistory.State == TextUndoHistoryState.Idle && + (e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive)) + { + // With projection, we sometimes get Changed events with no changes, or for "" -> "". + // We don't want to create undo actions for these. + bool nonNullChange = false; + foreach (ITextChange c in e.BeforeVersion.Changes) + { + if (c.OldLength != 0 || c.NewLength != 0) + { + nonNullChange = true; + break; + } + } + + if (nonNullChange) + { + // Queue the edit, and actually add an undo primitive later (see comment on PostChanged). + _editVersionList.Enqueue(e.BeforeVersion); + } + } + } + + /// <remarks> + /// Edits are queued up by our TextBufferChanged handler and then we finally add them to the + /// undo stack here in response to PostChanged. The reason and history behind why we do this + /// is as follows: + /// + /// Originally this was done for VB commit, which uses undo events (i.e. TransactionCompleted) to + /// trigger commit. Their commit logic relies on the buffer being in a state such that applying + /// an edit synchronously raises a Changed event (which is always the case for PostChanged, but + /// not for Changed if there are nested edits). + /// + /// JaredPar made a change (CS 1182244) that allowed VB to detect that UndoTransactionCompleted + /// was being fired from a nested edit, and therefore delay the actual commit until the following + /// PostChanged event. + /// + /// So this allowed us to move TextBufferUndoManager back to adding undo actions directly + /// from the TextBufferChanged handler (CS 1285117). This is preferable, as otherwise there's a + /// "delay" between when the edit happens and when we record the edit on the undo stack, + /// allowing other people to stick something on the undo stack (i.e. from + /// their ITextBuffer.Changed handler) in between. The result is actions being "out-of-order" + /// on the undo stack. + /// + /// Unfortunately, it turns out VB snippets actually rely on this "out-of-order" behavior + /// (see Dev10 834740) and so we are forced to revert CS 1285117) and return to the model + /// where we queue up edits and delay adding them to the undo stack until PostChanged. + /// + /// It would be good to revisit this at again, but we would need to work with VB + /// to fix their snippets / undo behavior, and verify that VB commit is also unaffected. + /// </remarks> + private void TextBufferPostChanged(object sender, EventArgs e) + { + // Only process a top level PostChanged event. Nested events will continue to process TextChange events + // which are added to the queue and will be processed below + if ( _inPostChanged ) + { + return; + } + + _inPostChanged = true; + try + { + // Do not do a foreach loop here. It's perfectly possible, and in fact expected, that the Complete + // method below can trigger a series of events which leads to a nested edit and another + // ITextBuffer::Changed. That event will add to the _editVersionList queue and hence break a + // foreach loop + while ( _editVersionList.Count > 0 ) + { + var cur = _editVersionList.Dequeue(); + using (ITextUndoTransaction undoTransaction = _undoHistory.CreateTransaction(Strings.TextBufferChanged)) + { + TextBufferChangeUndoPrimitive undoPrimitive = new TextBufferChangeUndoPrimitive(_undoHistory, cur); + undoTransaction.AddUndo(undoPrimitive); + + undoTransaction.Complete(); + } + } + } + finally + { + _editVersionList.Clear(); // Ensure we cleanup state in the face of an exception + _inPostChanged = false; + } + } + + void TextBufferChanging(object sender, TextContentChangingEventArgs e) + { + // See if somebody (other than us) is trying to edit the buffer during undo/redo. + if (_undoHistory.State != TextUndoHistoryState.Idle && + (e.EditTag as Type) != typeof(TextBufferChangeUndoPrimitive)) + { + Debug.Fail("Attempt to edit the buffer during undo/redo has been denied. This is explicitly prohibited as it would corrupt the undo stack! Please fix your code."); + e.Cancel(); + } + } + + + #endregion + + #region ITextBufferUndoManager Members + + public ITextBuffer TextBuffer + { + get { return _textBuffer; } + } + + public ITextUndoHistory TextBufferUndoHistory + { + // Note, right now, there is no way for us to know if an ITextUndoHistory + // has been unregistered (ie it can be unregistered by a third party) + // An issue has been logged with the Undo team, but in the mean time, to ensure that + // we are robust, always register the undo history. + get + { + _undoHistory = _undoHistoryRegistry.RegisterHistory(_textBuffer); + return _undoHistory; + } + } + + public void UnregisterUndoHistory() + { + // Unregister the undo history + _undoHistoryRegistry.RemoveHistory(_undoHistory); + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + _textBuffer.Changed -= TextBufferChanged; + _textBuffer.PostChanged -= TextBufferPostChanged; + _textBuffer.Changing -= TextBufferChanging; + + GC.SuppressFinalize(this); + } + + #endregion + } +} diff --git a/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs new file mode 100644 index 0000000..72eb736 --- /dev/null +++ b/src/Text/Impl/TextBufferUndoManager/TextBufferUndoManagerProvider.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.BufferUndoManager.Implementation +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Text.Operations; + using System.ComponentModel.Composition; + + [Export(typeof(ITextBufferUndoManagerProvider))] + internal sealed class TextBufferUndoManagerProvider : ITextBufferUndoManagerProvider + { + [Import] + internal ITextUndoHistoryRegistry _undoHistoryRegistry { get; set; } + + /// <summary> + /// Provides an <see cref="ITextBufferUndoManager"/> for the given <paramref name="textBuffer"/>. + /// </summary> + /// <param name="textBuffer">The <see cref="ITextBuffer"/> to create the <see cref="ITextBufferUndoManager"/> for.</param> + /// <returns>A cached <see cref="ITextBufferUndoManager"/> for the given <paramref name="textBuffer"/>.</returns> + /// <exception cref="ArgumentNullException"><paramref name="textBuffer" /> is null.</exception> + public ITextBufferUndoManager GetTextBufferUndoManager(ITextBuffer textBuffer) + { + // Validate + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + + // See if there was already a TextBufferUndoManager created for the given textBuffer, we only ever want to create one + ITextBufferUndoManager cachedBufferUndoManager; + if (!textBuffer.Properties.TryGetProperty<ITextBufferUndoManager>(typeof(ITextBufferUndoManager), out cachedBufferUndoManager)) + { + cachedBufferUndoManager = new TextBufferUndoManager(textBuffer, _undoHistoryRegistry); + textBuffer.Properties.AddProperty(typeof(ITextBufferUndoManager), cachedBufferUndoManager); + } + + return cachedBufferUndoManager; + } + + /// <summary> + /// If the specified <paramref name="textBuffer" /> has an <see cref="ITextBufferUndoManager" /> associated with it, remove it. + /// </summary> + /// <exception cref="ArgumentNullException"><paramref name="textBuffer" /> is null.</exception> + public void RemoveTextBufferUndoManager(ITextBuffer textBuffer) + { + // Validate + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + + ITextBufferUndoManager cachedBufferUndoManager; + if (textBuffer.Properties.TryGetProperty<ITextBufferUndoManager>(typeof(ITextBufferUndoManager), out cachedBufferUndoManager)) + { + // Dispose() so it stops listening to Changed events on the buffer. + IDisposable disposableBufferUndoManager = cachedBufferUndoManager as IDisposable; + if (disposableBufferUndoManager != null) + { + disposableBufferUndoManager.Dispose(); + } + + // Remove from cache. + textBuffer.Properties.RemoveProperty(typeof(ITextBufferUndoManager)); + } + } + } +} diff --git a/src/Text/Impl/TextModel/BaseBuffer.cs b/src/Text/Impl/TextModel/BaseBuffer.cs new file mode 100644 index 0000000..801dfbf --- /dev/null +++ b/src/Text/Impl/TextModel/BaseBuffer.cs @@ -0,0 +1,1120 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading; + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Threading; + using System.Threading.Tasks; + + internal abstract partial class BaseBuffer : ITextBuffer2 + { + #region ITextEventRaiser Interface + /// <summary> + /// Implemented internally to support a heterogeneous event queue. + /// </summary> + internal interface ITextEventRaiser + { + void RaiseEvent(BaseBuffer baseBuffer, bool immediate); + bool HasPostEvent { get; } + } + #endregion + + #region TextContentChangedEventRaiser Class + /// <summary> + /// The agent that knows how to raise ordinary TextContent changed events. + /// </summary> + internal class TextContentChangedEventRaiser : ITextEventRaiser + { + private TextContentChangedEventArgs args; + + public TextContentChangedEventRaiser(ITextSnapshot beforeSnapshot, + ITextSnapshot afterSnapshot, + EditOptions options, + Object editTag) + { + args = new TextContentChangedEventArgs(beforeSnapshot, afterSnapshot, options, editTag); + } + + public void RaiseEvent(BaseBuffer baseBuffer, bool immediate) + { + baseBuffer.RawRaiseEvent(args, immediate); + } + + public bool HasPostEvent + { + get { return true; } + } + } + #endregion + + #region TextBufferBaseEdit Class Definition + /// <summary> + /// Checking for edits already in progress and modifications on the proper thread. + /// </summary> + protected abstract class TextBufferBaseEdit : IDisposable + { + protected BaseBuffer baseBuffer; + protected bool applied; + protected bool canceled; + + public TextBufferBaseEdit(BaseBuffer baseBuffer) + { + this.baseBuffer = baseBuffer; + if (!baseBuffer.CheckEditAccess()) + { + throw new InvalidOperationException(Strings.InvalidTextBufferEditThread); + } + if (baseBuffer.editInProgress) + { + throw new InvalidOperationException(Strings.SimultaneousEdit); + } + baseBuffer.editInProgress = true; + baseBuffer.group.BeginEdit(); + } + + public virtual void Cancel() + { + this.CancelApplication(); + } + + public virtual void CancelApplication() + { + if (!this.canceled) + { + this.canceled = true; + this.baseBuffer.editInProgress = false; + this.baseBuffer.group.CancelEdit(); + } + } + + public bool Canceled + { + get + { + return this.canceled; + } + } + + public void Dispose() + { + if (!this.applied && !this.canceled) + { + this.CancelApplication(); + } + GC.SuppressFinalize(this); + } + } + #endregion + + #region TextBufferEdit Class Definition + /// <summary> + /// Edit protocol checking. + /// </summary> + protected abstract partial class TextBufferEdit : TextBufferBaseEdit + { + protected ITextSnapshot originSnapshot; + protected object editTag; + + public TextBufferEdit(BaseBuffer baseBuffer, ITextSnapshot snapshot, object editTag) + : base(baseBuffer) + { + this.baseBuffer = baseBuffer; + this.originSnapshot = snapshot; + this.editTag = editTag; + } + + public ITextSnapshot Snapshot + { + get { return this.originSnapshot; } + } + + public ITextSnapshot Apply() + { + ITextSnapshot snapshot; + try + { + snapshot = PerformApply(); + } + finally + { + // TextBufferBaseEdit.Cancel may have been called via + // the cancellable Changing event. In that case, group.CancelEdit will + // have been called and canceled will be true. + if (!this.canceled) + { + this.baseBuffer.group.FinishEdit(); + } + } + + return snapshot; + } + + protected abstract ITextSnapshot PerformApply(); + + protected void CheckActive() + { + if (this.canceled) + { + throw new InvalidOperationException(Strings.ContinueCanceledEdit); + } + if (this.applied) + { + throw new InvalidOperationException(Strings.ReuseAppliedEdit); + } + } + } + #endregion + + #region Edit Class Definition + /// <summary> + /// Fundamental editing operations. + /// </summary> + protected abstract partial class Edit : TextBufferEdit, ITextEdit + { + private readonly int bufferLength; + protected FrugalList<TextChange> changes; + protected readonly EditOptions options; + protected readonly int? reiteratedVersionNumber; + private TextContentChangingEventArgs raisedChangingEventArgs; + private Action cancelAction; + private bool hasFailedChanges; + + protected Edit(BaseBuffer baseBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, Object editTag) + : base(baseBuffer, originSnapshot, editTag) + { + this.bufferLength = originSnapshot.Length; + this.changes = new FrugalList<TextChange>(); + this.options = options; + this.reiteratedVersionNumber = reiteratedVersionNumber; + this.raisedChangingEventArgs = null; + this.cancelAction = null; + this.hasFailedChanges = false; + } + + public bool Insert(int position, string text) + { + CheckActive(); + if (position < 0 || position > this.bufferLength) + { + throw new ArgumentOutOfRangeException("position"); + } + if (text == null) + { + throw new ArgumentNullException("text"); + } + + // Check for ReadOnly + if (this.baseBuffer.IsReadOnlyImplementation(position, isEdit: true)) + { + this.hasFailedChanges = true; + return false; + } + + if (text.Length != 0) + { + this.changes.Add(TextChange.Create(position, string.Empty, text, this.originSnapshot)); + } + return true; + } + + public bool Insert(int position, char[] characterBuffer, int startIndex, int length) + { + CheckActive(); + if (position < 0 || position > this.bufferLength) + { + throw new ArgumentOutOfRangeException("position"); + } + if (characterBuffer == null) + { + throw new ArgumentNullException("characterBuffer"); + } + if (startIndex < 0 || startIndex > characterBuffer.Length) + { + throw new ArgumentOutOfRangeException("startIndex"); + } + if (length < 0 || startIndex + length > characterBuffer.Length) + { + throw new ArgumentOutOfRangeException("length"); + } + + // Check for ReadOnly + if (this.baseBuffer.IsReadOnlyImplementation(position, isEdit: true)) + { + this.hasFailedChanges = true; + return false; + } + + if (length != 0) + { + this.changes.Add(TextChange.Create(position, string.Empty, new string(characterBuffer, startIndex, length), this.originSnapshot)); + } + return true; + } + + public bool Replace(int startPosition, int charsToReplace, string replaceWith) + { + CheckActive(); + if (startPosition < 0 || startPosition > this.bufferLength) + { + throw new ArgumentOutOfRangeException("startPosition"); + } + if (charsToReplace < 0 || startPosition + charsToReplace > this.bufferLength) + { + throw new ArgumentOutOfRangeException("charsToReplace"); + } + if (replaceWith == null) + { + throw new ArgumentNullException("replaceWith"); + } + + // Check for ReadOnly + if (this.baseBuffer.IsReadOnlyImplementation(new Span(startPosition, charsToReplace), isEdit: true)) + { + this.hasFailedChanges = true; + return false; + } + + if (charsToReplace != 0 || replaceWith.Length != 0) + { + this.changes.Add(TextChange.Create(startPosition, DeletionChangeString(new Span(startPosition, charsToReplace)), replaceWith, this.originSnapshot)); + } + return true; + } + + public bool Replace(Span replaceSpan, string replaceWith) + { + CheckActive(); + if (replaceSpan.End > this.bufferLength) + { + throw new ArgumentOutOfRangeException("replaceSpan"); + } + if (replaceWith == null) + { + throw new ArgumentNullException("replaceWith"); + } + + // Check for ReadOnly + if (this.baseBuffer.IsReadOnlyImplementation(replaceSpan, isEdit: true)) + { + this.hasFailedChanges = true; + return false; + } + + if (replaceSpan.Length != 0 || replaceWith.Length != 0) + { + this.changes.Add(TextChange.Create(replaceSpan.Start, DeletionChangeString(replaceSpan), replaceWith, this.originSnapshot)); + } + return true; + } + + public bool Delete(int startPosition, int charsToDelete) + { + CheckActive(); + if (startPosition < 0 || startPosition > this.bufferLength) + { + throw new ArgumentOutOfRangeException("startPosition"); + } + if (charsToDelete < 0 || startPosition + charsToDelete > this.bufferLength) + { + throw new ArgumentOutOfRangeException("charsToDelete"); + } + + // Check for ReadOnly + if (this.baseBuffer.IsReadOnlyImplementation(new Span(startPosition, charsToDelete), isEdit: true)) + { + this.hasFailedChanges = true; + return false; + } + + if (charsToDelete != 0) + { + this.changes.Add(TextChange.Create(startPosition, DeletionChangeString(new Span(startPosition, charsToDelete)), StringRebuilder.Empty, this.originSnapshot)); + } + return true; + } + + public bool Delete(Span deleteSpan) + { + CheckActive(); + if (deleteSpan.End > this.bufferLength) + { + throw new ArgumentOutOfRangeException("deleteSpan"); + } + + // Check for ReadOnly + if (this.baseBuffer.IsReadOnlyImplementation(deleteSpan, isEdit: true)) + { + this.hasFailedChanges = true; + return false; + } + + if (deleteSpan.Length != 0) + { + this.changes.Add(TextChange.Create(deleteSpan.Start, DeletionChangeString(deleteSpan), StringRebuilder.Empty, this.originSnapshot)); + } + return true; + } + + private StringRebuilder DeletionChangeString(Span deleteSpan) + { + return BufferFactoryService.StringRebuilderFromSnapshotAndSpan(this.originSnapshot, deleteSpan); + } + + /// <summary> + /// Checks whether the edit on the buffer is allowed to continue. + /// </summary> + /// <param name="cancelationResponse">Additional action to perform if the edit itself is canceled.</param> + public bool CheckForCancellation(Action cancelationResponse) + { + Debug.Assert(this.raisedChangingEventArgs == null, "just checking"); + + // If no changes are being applied to this edit's buffer then there will be no new snapshot produced and + // the Changed event won't be raised and so the cancelable Changing event should not be raised either. + if (this.changes.Count == 0) + { + return true; + } + + if (this.raisedChangingEventArgs == null) + { + this.cancelAction = cancelationResponse; + this.raisedChangingEventArgs = new TextContentChangingEventArgs(this.Snapshot, this.editTag, (args) => + { + this.Cancel(); + }); + this.baseBuffer.RaiseChangingEvent(this.raisedChangingEventArgs); + } + this.canceled = this.raisedChangingEventArgs.Canceled; + //Debug.Assert(!this.canceled || !this.applied, "an edit shouldn't be both canceled and applied"); + return !this.raisedChangingEventArgs.Canceled; + } + + public override void Cancel() + { + base.Cancel(); + + if (this.cancelAction != null) + { + this.cancelAction(); + } + } + + public bool HasEffectiveChanges + { + get + { + return this.changes.Count > 0; + } + } + + public bool HasFailedChanges + { + get + { + return this.hasFailedChanges; + } + } + + public override string ToString() + { + System.Text.StringBuilder builder = new System.Text.StringBuilder(); + for (int c = 0; c < this.changes.Count; ++c) + { + TextChange change = this.changes[c]; + builder.Append(change.ToString(brief: true)); + if (c < this.changes.Count - 1) + { + builder.Append("\r\n"); + } + } + return builder.ToString(); + } + + public void RecordMasterChangeOffset(int masterChangeOffset) + { + if (this.changes.Count == 0) + { + throw new InvalidOperationException("Can't record a change offset without a change."); + } + + this.changes[this.changes.Count - 1].RecordMasterChangeOffset(masterChangeOffset); + } + } + #endregion // Edit Class Definition + + #region Read Only Region Edit Class Definition + private sealed partial class ReadOnlyRegionEdit : TextBufferEdit, IReadOnlyRegionEdit + { + private List<IReadOnlyRegion> readOnlyRegionsToAdd = new List<IReadOnlyRegion>(); + private List<IReadOnlyRegion> readOnlyRegionsToRemove = new List<IReadOnlyRegion>(); + + private int aggregateEnd = int.MinValue; + private int aggregateStart = int.MaxValue; + + public ReadOnlyRegionEdit(BaseBuffer baseBuffer, ITextSnapshot originSnapshot, Object editTag) + : base(baseBuffer, originSnapshot, editTag) + { + } + + protected override ITextSnapshot PerformApply() + { + CheckActive(); + + this.applied = true; + + if ((this.readOnlyRegionsToAdd.Count > 0) || (this.readOnlyRegionsToRemove.Count > 0)) + { + if (this.readOnlyRegionsToAdd.Count > 0) + { + // We leave the read only regions collection on the buffer null + // since most buffers will never have a read only region. Since + // regions are being added, create it now. + if (this.baseBuffer.readOnlyRegions == null) + { + this.baseBuffer.readOnlyRegions = new FrugalList<IReadOnlyRegion>(); + } + + this.baseBuffer.readOnlyRegions.AddRange(this.readOnlyRegionsToAdd); + } + + if (this.readOnlyRegionsToRemove.Count > 0) + { + // We've already verified that it makes sense to remove these read only + // regions, so just proceed without further checks. + foreach (IReadOnlyRegion readOnlyRegion in this.readOnlyRegionsToRemove) + { + this.baseBuffer.readOnlyRegions.Remove(readOnlyRegion); + } + } + + // Save off the current state of the read only spans + this.baseBuffer.readOnlyRegionSpanCollection = new ReadOnlySpanCollection(this.baseBuffer.CurrentVersion, this.baseBuffer.readOnlyRegions); + + ReadOnlyRegionsChangedEventRaiser raiser = + new ReadOnlyRegionsChangedEventRaiser(new SnapshotSpan(this.baseBuffer.CurrentSnapshot, this.aggregateStart, this.aggregateEnd - this.aggregateStart)); + this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer); + // no immediate event for read only regions + this.baseBuffer.editInProgress = false; + } + else + { + this.baseBuffer.editInProgress = false; + } + + // no new snapshot + return this.originSnapshot; + } + + #region IReadOnlyRegionEdit Members + + public IReadOnlyRegion CreateReadOnlyRegion(Span span, SpanTrackingMode trackingMode, EdgeInsertionMode edgeInsertionMode) + { + return CreateDynamicReadOnlyRegion(span, trackingMode, edgeInsertionMode, callback: null); + } + + public IReadOnlyRegion CreateDynamicReadOnlyRegion(Span span, SpanTrackingMode trackingMode, EdgeInsertionMode edgeInsertionMode, DynamicReadOnlyRegionQuery callback) + { + ReadOnlyRegion readOnlyRegion = new ReadOnlyRegion(this.baseBuffer.CurrentVersion, span, trackingMode, edgeInsertionMode, callback); + + readOnlyRegionsToAdd.Add(readOnlyRegion); + + this.aggregateStart = Math.Min(this.aggregateStart, span.Start); + this.aggregateEnd = Math.Max(this.aggregateEnd, span.End); + + return readOnlyRegion; + } + + public IReadOnlyRegion CreateReadOnlyRegion(Span span) + { + return CreateReadOnlyRegion(span, SpanTrackingMode.EdgeExclusive, EdgeInsertionMode.Allow); + } + + public void RemoveReadOnlyRegion(IReadOnlyRegion readOnlyRegion) + { + // Throw if trying to remove a region if there aren't that many regions created. + if (this.baseBuffer.readOnlyRegions == null) + { + throw new InvalidOperationException(Strings.RemoveNoReadOnlyRegion); + } + + // Throw if trying to remove a region from the wrong buffer + if (this.readOnlyRegionsToRemove.Exists(delegate(IReadOnlyRegion match) { return !object.ReferenceEquals(match.Span.TextBuffer, this.baseBuffer); })) + { + throw new InvalidOperationException(Strings.InvalidReadOnlyRegion); + } + + this.readOnlyRegionsToRemove.Add(readOnlyRegion); + + Span regionSpan = readOnlyRegion.Span.GetSpan(this.baseBuffer.CurrentSnapshot); + this.aggregateStart = Math.Min(this.aggregateStart, regionSpan.Start); + this.aggregateEnd = Math.Max(this.aggregateEnd, regionSpan.End); + } + + #endregion + } + #endregion // Read Only Region Edit Class Definition + + #region ContentType Edit Class Definition + private sealed class ContentTypeEdit : TextBufferEdit, ISubordinateTextEdit + { + private IContentType _newContentType; + + public ContentTypeEdit(BaseBuffer baseBuffer, ITextSnapshot originSnapshot, Object editTag, IContentType newContentType) + : base(baseBuffer, originSnapshot, editTag) + { + _newContentType = newContentType; + } + + public ITextBuffer TextBuffer + { + get { return this.baseBuffer; } + } + + protected override ITextSnapshot PerformApply() + { + CheckActive(); + + this.applied = true; + + if (_newContentType != null) + { + // we need to perform a group edit because any projection buffers that use this buffer will + // generate new snapshots as independent edits. + this.baseBuffer.group.PerformMasterEdit(this.baseBuffer, this, EditOptions.None, this.editTag); + } + else + { + this.baseBuffer.editInProgress = false; + } + + return this.baseBuffer.currentSnapshot; + } + + public void PreApply() + { + // all the action is in FinalApply() + } + + public void FinalApply() + { + IContentType beforeContentType = baseBuffer.contentType; + this.baseBuffer.contentType = _newContentType; + this.baseBuffer.SetCurrentVersionAndSnapshot(NormalizedTextChangeCollection.Empty); + ITextEventRaiser raiser = new ContentTypeChangedEventRaiser(this.originSnapshot, baseBuffer.currentSnapshot, beforeContentType, baseBuffer.contentType, editTag); + this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer); + raiser.RaiseEvent(this.baseBuffer, true); + this.baseBuffer.editInProgress = false; + } + + public bool CheckForCancellation(Action cancelAction) + { + // Not cancelable. + return true; + } + + public void RecordMasterChangeOffset(int masterChangeOffset) + { + throw new InvalidOperationException("Content type edits shouldn't have change offsets."); + } + } + #endregion // IContentType Edit Class Definition + + #region Private members and construction + private IContentType contentType; + private PropertyCollection properties; + private readonly Object syncLock = new Object(); + private Thread editThread; + protected internal BufferGroup group; + protected internal StringRebuilder builder; + protected internal BaseSnapshot currentSnapshot; + protected TextVersion currentVersion; + private FrugalList<IReadOnlyRegion> readOnlyRegions; + protected ReadOnlySpanCollection readOnlyRegionSpanCollection; + protected internal bool editInProgress; + protected internal ITextDifferencingService textDifferencingService; + protected readonly GuardedOperations guardedOperations; + + private static bool eventTracing = false; + private static int eventDepth = 0; + + protected BaseBuffer(IContentType contentType, int initialLength, ITextDifferencingService textDifferencingService, GuardedOperations guardedOperations) + { + // parameters are validated outside + Debug.Assert(contentType != null); + + this.contentType = contentType; + this.currentVersion = new TextVersion(this, new TextImageVersion(initialLength)); + // this.builder should be set in calling ctor + this.textDifferencingService = textDifferencingService; + this.guardedOperations = guardedOperations; + } + #endregion + + #region ITextBuffer members + public IContentType ContentType + { + get { return this.contentType; } + } + + public PropertyCollection Properties + { + get + { + if (this.properties == null) + { + lock (this.syncLock) + { + if (this.properties == null) + { + this.properties = new PropertyCollection(); + } + } + } + return this.properties; + } + } + + public ITextSnapshot CurrentSnapshot + { + get { return this.currentSnapshot; } + } + + protected TextVersion CurrentVersion + { + get { return this.currentVersion; } + } + + public abstract ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag); + + public ITextEdit CreateEdit() + { + return CreateEdit(EditOptions.None, null, null); + } + + public IReadOnlyRegionEdit CreateReadOnlyRegionEdit() + { + return CreateReadOnlyRegionEdit(null); + } + + public IReadOnlyRegionEdit CreateReadOnlyRegionEdit(object editTag) + { + return new ReadOnlyRegionEdit(this, this.CurrentSnapshot, editTag); + } + + public void ChangeContentType(IContentType newContentType, object editTag) + { + if (newContentType == null) + { + throw new ArgumentNullException("newContentType"); + } + + if (newContentType != this.contentType) + { + using (ContentTypeEdit edit = new ContentTypeEdit(this, this.currentSnapshot, editTag, newContentType)) + { + edit.Apply(); + } + } + } + + public bool EditInProgress + { + get { return this.editInProgress; } + } + + public void TakeThreadOwnership() + { + lock (this.syncLock) + { + if (this.editThread != null && this.editThread != Thread.CurrentThread) + { + throw new InvalidOperationException(Strings.InvalidBufferThreadOwnershipChange); + } + this.editThread = Thread.CurrentThread; + } + } + + public bool CheckEditAccess() + { + return this.editThread == null || this.editThread == Thread.CurrentThread; + } + + protected abstract BaseSnapshot TakeSnapshot(); + #endregion + + #region ReadOnlyRegion support + public bool IsReadOnly(int position) + { + return IsReadOnly(position, isEdit: false); + } + + public bool IsReadOnly(int position, bool isEdit) + { + ReadOnlyQueryThreadCheck(); + if ((position < 0) || (position > this.currentSnapshot.Length)) + { + throw new ArgumentOutOfRangeException("position"); + } + + return IsReadOnlyImplementation(position, isEdit); + } + + public bool IsReadOnly(Span span) + { + return IsReadOnly(span, isEdit: false); + } + + public bool IsReadOnly(Span span, bool isEdit) + { + ReadOnlyQueryThreadCheck(); + if (span.End > this.currentSnapshot.Length) + { + throw new ArgumentOutOfRangeException("span"); + } + + return IsReadOnlyImplementation(span, isEdit); + } + + protected internal virtual bool IsReadOnlyImplementation(int position, bool isEdit) + { + if (this.readOnlyRegionSpanCollection == null) + { + return false; + } + return this.readOnlyRegionSpanCollection.IsReadOnly(position, this.currentSnapshot, isEdit); + } + + protected internal virtual bool IsReadOnlyImplementation(Span span, bool isEdit) + { + if (this.readOnlyRegionSpanCollection == null) + { + return false; + } + return this.readOnlyRegionSpanCollection.IsReadOnly(span, this.currentSnapshot, isEdit); + } + + public NormalizedSpanCollection GetReadOnlyExtents(Span span) + { + ReadOnlyQueryThreadCheck(); + if (span.End > this.CurrentSnapshot.Length) + { + throw new ArgumentOutOfRangeException("span"); + } + return GetReadOnlyExtentsImplementation(span); + } + + protected internal virtual NormalizedSpanCollection GetReadOnlyExtentsImplementation(Span span) + { + FrugalList<Span> spans = new FrugalList<Span>(); + + if (this.readOnlyRegionSpanCollection != null) + { + foreach (ReadOnlySpan readOnlySpan in this.readOnlyRegionSpanCollection.QueryAllEffectiveReadOnlySpans(this.currentVersion)) + { + Span readOnlySpanSpan = readOnlySpan.GetSpan(this.currentSnapshot); + Span? overlapSpan = (readOnlySpanSpan == span) ? readOnlySpanSpan : readOnlySpanSpan.Overlap(span); + if (overlapSpan.HasValue) + { + spans.Add(overlapSpan.Value); + } + } + } + + return new NormalizedSpanCollection(spans); + } + + private void ReadOnlyQueryThreadCheck() + { + if (!CheckEditAccess()) + { + throw new InvalidOperationException(Strings.InvalidTextBufferEditThread); + } + } + + public event EventHandler<SnapshotSpanEventArgs> ReadOnlyRegionsChanged; + + protected class ReadOnlyRegionsChangedEventRaiser : ITextEventRaiser + { + private SnapshotSpan affectedSpan; + + public ReadOnlyRegionsChangedEventRaiser(SnapshotSpan affectedSpan) + { + this.affectedSpan = affectedSpan; + } + + public void RaiseEvent(BaseBuffer baseBuffer, bool immediate) + { + // there is no immediate form of this event since it does not create a snapshot + Debug.Assert(!immediate); + EventHandler<SnapshotSpanEventArgs> handler = baseBuffer.ReadOnlyRegionsChanged; + if (handler != null) + { + var args = new SnapshotSpanEventArgs(affectedSpan); + baseBuffer.guardedOperations.RaiseEvent(baseBuffer, handler, args); + } + } + + public bool HasPostEvent + { + get { return false; } + } + } + #endregion + + #region IContentType change support + public event EventHandler<ContentTypeChangedEventArgs> ContentTypeChanged; + + protected class ContentTypeChangedEventRaiser : ITextEventRaiser + { + #region Private Members + ITextSnapshot beforeSnapshot; + ITextSnapshot afterSnapshot; + object editTag; + IContentType beforeContentType; + IContentType afterContentType; + #endregion + + public ContentTypeChangedEventRaiser(ITextSnapshot beforeSnapshot, ITextSnapshot afterSnapshot, IContentType beforeContentType, IContentType afterContentType, object editTag) + { + this.beforeSnapshot = beforeSnapshot; + this.afterSnapshot = afterSnapshot; + this.editTag = editTag; + this.beforeContentType = beforeContentType; + this.afterContentType = afterContentType; + } + + public void RaiseEvent(BaseBuffer baseBuffer, bool immediate) + { + EventHandler<ContentTypeChangedEventArgs> handler = immediate ? baseBuffer.ContentTypeChangedImmediate : baseBuffer.ContentTypeChanged; + if (handler != null) + { + var eventArgs = new ContentTypeChangedEventArgs(this.beforeSnapshot, this.afterSnapshot, this.beforeContentType, this.afterContentType, this.editTag); + baseBuffer.guardedOperations.RaiseEvent(baseBuffer, handler, eventArgs); + } + } + + public bool HasPostEvent + { + get { return true; } + } + } + #endregion + + #region Editing Shortcuts + public ITextSnapshot Insert(int position, string text) + { + using (ITextEdit textEdit = CreateEdit()) + { + textEdit.Insert(position, text); + return textEdit.Apply(); + } + } + + public ITextSnapshot Delete(Span deleteSpan) + { + using (ITextEdit textEdit = CreateEdit()) + { + textEdit.Delete(deleteSpan); + return textEdit.Apply(); + } + } + + public ITextSnapshot Replace(Span replaceSpan, string replaceWith) + { + using (ITextEdit textEdit = CreateEdit()) + { + textEdit.Replace(replaceSpan, replaceWith); + return textEdit.Apply(); + } + } + #endregion + + #region Change Application and Eventing + protected void SetCurrentVersionAndSnapshot(INormalizedTextChangeCollection normalizedChanges, int reiteratedVersionNumber = -1) + { + this.currentVersion = this.currentVersion.CreateNext(normalizedChanges, newLength: -1, reiteratedVersionNumber: reiteratedVersionNumber); + this.builder = this.ApplyChangesToStringRebuilder(normalizedChanges, this.builder); + this.currentSnapshot = TakeSnapshot(); + } + + public StringRebuilder ApplyChangesToStringRebuilder(INormalizedTextChangeCollection normalizedChanges, StringRebuilder source) + { + var doppelganger = this.GetDoppelgangerBuilder(); + if (doppelganger != null) + return doppelganger; + + for (int i = normalizedChanges.Count - 1; (i >= 0); --i) + { + ITextChange change = normalizedChanges[i]; + source = source.Replace(change.OldSpan, TextChange.NewStringRebuilder(change)); + } + + return source; + } + + protected internal abstract ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag); + protected virtual StringRebuilder GetDoppelgangerBuilder() { return null; } + + public event EventHandler<TextContentChangingEventArgs> Changing; + + public event EventHandler<TextContentChangedEventArgs> ChangedHighPriority; + public event EventHandler<TextContentChangedEventArgs> Changed; + public event EventHandler<TextContentChangedEventArgs> ChangedLowPriority; + + public event EventHandler PostChanged; + public event EventHandler<TextContentChangedEventArgs> ChangedOnBackground; + private Task _lastChangeOnBackgroundRaisedEvent = TextUtilities.CompletedNonInliningTask; + + internal event EventHandler<TextContentChangedEventArgs> ChangedImmediate; + internal event EventHandler<ContentTypeChangedEventArgs> ContentTypeChangedImmediate; + + internal void RawRaiseEvent(TextContentChangedEventArgs args, bool immediate) + { + if (immediate) + { + EventHandler<TextContentChangedEventArgs> immediateHandler = ChangedImmediate; + if (immediateHandler != null) + { + if (BaseBuffer.eventTracing) + { + Debug.WriteLine("<<< Imm events from " + ToString()); + } + immediateHandler(this, args); + } + return; + } + + EventHandler<TextContentChangedEventArgs> highHandler = ChangedHighPriority; + EventHandler<TextContentChangedEventArgs> medHandler = Changed; + EventHandler<TextContentChangedEventArgs> lowHandler = ChangedLowPriority; + EventHandler<TextContentChangedEventArgs> changedOnBackgroundHandler = ChangedOnBackground; + + BaseBuffer.eventDepth++; + string indent = BaseBuffer.eventTracing ? new String(' ', 3 * (BaseBuffer.eventDepth - 1)) : null; + if (highHandler != null) + { + if (BaseBuffer.eventTracing) + { + Debug.WriteLine(">>> " + indent + "High events from " + ToString()); + } + this.guardedOperations.RaiseEvent(this, highHandler, args); + } + if (changedOnBackgroundHandler != null) + { + if (BaseBuffer.eventTracing) + { + Debug.WriteLine(">>> " + indent + "background events from " + ToString()); + } + + // As this is a background event, we need to make sure handlers are executed synchronized + // and in the order the edits were applied. + // TODO: with this implementation any handler might delay all subsequent handlers. + // That's true for other Changed* events too, but this event is raised on a background thread + // so introducing delays in a handler won't be that easily noticable, also being on a + // background thread might suggest it's actually ok to perform some long running + // calculation directly in the handler. + // For isolation purposes we need a chain of tasks per handler, or some other, more + // optimized isolation strategy. Tracked by #449694. + _lastChangeOnBackgroundRaisedEvent = _lastChangeOnBackgroundRaisedEvent + .ContinueWith(_ => + { + // changedOnBackgroundHandler might be stale at this point, get the latest list of handlers + var currentChangedOnBackgroundHandler = ChangedOnBackground; + if (currentChangedOnBackgroundHandler != null) + { + this.guardedOperations.RaiseEvent(this, currentChangedOnBackgroundHandler, args); + } + }, + CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + // Now register pending task with task tracker to ensure it's completed when editor host is shutdown + this.guardedOperations.NonJoinableTaskTracker?.Register(_lastChangeOnBackgroundRaisedEvent); + } + if (medHandler != null) + { + if (BaseBuffer.eventTracing) + { + Debug.WriteLine(">>> " + indent + "Med events from " + ToString()); + } + this.guardedOperations.RaiseEvent(this, medHandler, args); + } + if (lowHandler != null) + { + if (BaseBuffer.eventTracing) + { + Debug.WriteLine(">>> " + indent + "Low events from " + ToString()); + } + this.guardedOperations.RaiseEvent(this, lowHandler, args); + } + BaseBuffer.eventDepth--; + } + + internal void RaisePostChangedEvent() + { + this.guardedOperations.RaiseEvent(this, PostChanged); + } + + internal void RaiseChangingEvent(TextContentChangingEventArgs args) + { + var changing = this.Changing; + + if (changing != null) + { + foreach (Delegate handlerDelegate in changing.GetInvocationList()) + { + var handler = (EventHandler<TextContentChangingEventArgs>)handlerDelegate; + try + { + handler(this, args); + } + catch (Exception e) + { + this.guardedOperations.HandleException(handler, e); + } + + if (args.Canceled) + return; + } + } + } + #endregion + + #region Diagnostic Support + + public override string ToString() + { + string suffix = TextUtilities.GetTag(this); + if (string.IsNullOrEmpty(suffix)) + { + ITextSnapshot snap = this.currentSnapshot; + if (snap != null) + { + suffix = "\"" + snap.GetText(0, Math.Min(16, snap.Length)) + "\""; + } + } + return this.ContentType.TypeName + ":" + suffix; + } + +#if _DEBUG + private string DebugOnly_AllText + { + get + { + return this.currentSnapshot.DebugOnly_AllText; + } + } +#endif + + #endregion + } +} diff --git a/src/Text/Impl/TextModel/BaseSnapshot.cs b/src/Text/Impl/TextModel/BaseSnapshot.cs new file mode 100644 index 0000000..db723d3 --- /dev/null +++ b/src/Text/Impl/TextModel/BaseSnapshot.cs @@ -0,0 +1,203 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Text; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Base class for all varieties of Text Snapshots. + /// </summary> + internal abstract class BaseSnapshot : ITextSnapshot, ITextSnapshot2 + { + #region State and Construction + protected readonly ITextVersion2 version; + private readonly IContentType contentType; + + internal readonly StringRebuilder Content; + internal readonly ITextImage cachingContent; + + protected BaseSnapshot(ITextVersion2 version, StringRebuilder content) + { + this.version = version; + this.Content = content; + this.cachingContent = CachingTextImage.Create(this.Content, version.ImageVersion); + + // we must extract the content type here, because the content type of the text buffer may change later. + this.contentType = version.TextBuffer.ContentType; + } + #endregion + + #region ITextSnapshot implementations + + public ITextBuffer TextBuffer + { + get { return this.TextBufferHelper; } + } + + public IContentType ContentType + { + get { return this.contentType; } + } + + public ITextVersion Version + { + get { return this.version; } + } + + public string GetText(int startIndex, int length) + { + return GetText(new Span(startIndex, length)); + } + + public string GetText() + { + return GetText(new Span(0, this.Length)); + } + + #region Point and Span factories + public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode) + { + return this.version.CreateTrackingPoint(position, trackingMode); + } + + public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + return this.version.CreateTrackingPoint(position, trackingMode, trackingFidelity); + } + + public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode) + { + return this.version.CreateTrackingSpan(start, length, trackingMode); + } + + public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + return this.version.CreateTrackingSpan(start, length, trackingMode, trackingFidelity); + } + + public ITrackingSpan CreateTrackingSpan(Span span, SpanTrackingMode trackingMode) + { + return this.version.CreateTrackingSpan(span, trackingMode, TrackingFidelityMode.Forward); + } + + public ITrackingSpan CreateTrackingSpan(Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + return this.version.CreateTrackingSpan(span, trackingMode, trackingFidelity); + } + #endregion + #endregion + + #region ITextSnapshot2 implementations + public void SaveToFile(string filePath, bool replaceFile, Encoding encoding) + { + FileUtilities.SaveSnapshot(this, replaceFile ? FileMode.Create : FileMode.CreateNew, encoding, filePath); + } + #endregion + + #region ITextSnapshot abstract methods + protected abstract ITextBuffer TextBufferHelper { get; } + + public int Length + { + get { return this.cachingContent.Length; } + } + + public int LineCount + { + get { return this.cachingContent.LineCount; } + } + + public string GetText(Span span) + { + return this.cachingContent.GetText(span); + } + + public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) + { + this.cachingContent.CopyTo(sourceIndex, destination, destinationIndex, count); + } + + public char[] ToCharArray(int startIndex, int length) + { + return this.cachingContent.ToCharArray(startIndex, length); + } + + public char this[int position] + { + get + { + return this.cachingContent[position]; + } + } + + public ITextSnapshotLine GetLineFromLineNumber(int lineNumber) + { + TextImageLine lineSpan = this.cachingContent.GetLineFromLineNumber(lineNumber); + + return new TextSnapshotLine(this, lineSpan); + } + + public ITextSnapshotLine GetLineFromPosition(int position) + { + int lineNumber = this.cachingContent.GetLineNumberFromPosition(position); + return this.GetLineFromLineNumber(lineNumber); + } + + public int GetLineNumberFromPosition(int position) + { + return this.cachingContent.GetLineNumberFromPosition(position); + } + + public IEnumerable<ITextSnapshotLine> Lines + { + get + { + // this is a naive implementation + int lineCount = this.cachingContent.LineCount; + for (int line = 0; line < lineCount; ++line) + { + yield return GetLineFromLineNumber(line); + } + } + } + + public void Write(System.IO.TextWriter writer) + { + this.cachingContent.Write(writer, new Span(0, this.cachingContent.Length)); + } + + public void Write(System.IO.TextWriter writer, Span span) + { + this.cachingContent.Write(writer, span); + } + #endregion + + public ITextImage TextImage => this.cachingContent; + + public override string ToString() + { + return String.Format("version: {0} lines: {1} length: {2} \r\n content: {3}", + Version.VersionNumber, LineCount, Length, + Microsoft.VisualStudio.Text.Utilities.TextUtilities.Escape(this.GetText(0, Math.Min(40, this.Length)))); + } + +#if _DEBUG + internal string DebugOnly_AllText + { + get + { + return this.GetText(0, Math.Min(this.Length, 1024*1024)); + } + } +#endif + } +} diff --git a/src/Text/Impl/TextModel/BufferFactoryService.cs b/src/Text/Impl/TextModel/BufferFactoryService.cs new file mode 100644 index 0000000..975cbf0 --- /dev/null +++ b/src/Text/Impl/TextModel/BufferFactoryService.cs @@ -0,0 +1,409 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.ComponentModel.Composition; + using System.Diagnostics; + using System.IO; + using System.IO.MemoryMappedFiles; + using System.Text; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Text.Projection.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Factory for TextBuffers and ProjectionBuffers. + /// </summary> + [Export(typeof(ITextImageFactoryService))] + [Export(typeof(ITextImageFactoryService2))] + [Export(typeof(ITextBufferFactoryService))] + [Export(typeof(ITextBufferFactoryService2))] + [Export(typeof(ITextBufferFactoryService3))] + [Export(typeof(IProjectionBufferFactoryService))] + internal partial class BufferFactoryService : ITextBufferFactoryService2, ITextBufferFactoryService3, IProjectionBufferFactoryService, IInternalTextBufferFactory, ITextImageFactoryService2 + { + #region Standard Content Type Definitions + [Export] + [Name("any")] + public ContentTypeDefinition anyContentTypeDefinition; + + [Export] + [Name("text")] + [BaseDefinition("any")] + public ContentTypeDefinition textContentTypeDefinition; + + [Export] + [Name("projection")] + [BaseDefinition("any")] + public ContentTypeDefinition projectionContentTypeDefinition; + + [Export] + [Name("plaintext")] + [BaseDefinition("text")] + public ContentTypeDefinition plaintextContentTypeDefinition; + + [Export] + [Name("code")] + [BaseDefinition("text")] + public ContentTypeDefinition codeContentType; + + [Export] + [Name("inert")] + // N.B.: This ContentType does NOT inherit from anything + public ContentTypeDefinition inertContentTypeDefinition; + #endregion + + #region Service Consumptions + + [Import] + internal IContentTypeRegistryService _contentTypeRegistryService { get; set; } + + [Import] + internal IDifferenceService _differenceService { get; set; } + + [Import] + internal ITextDifferencingSelectorService _textDifferencingSelectorService { get; set; } + + [Import] + internal GuardedOperations _guardedOperations { get; set; } + + #endregion + + #region Private state + private IContentType textContentType; + private IContentType plaintextContentType; + private IContentType inertContentType; + private IContentType projectionContentType; + #endregion + + #region ContentType accessors + public IContentType TextContentType + { + get + { + if (this.textContentType == null) + { + // it's OK to evaluate this more than once, and the assignment is atomic, so we don't protect this with a lock + this.textContentType = _contentTypeRegistryService.GetContentType("text"); + } + return this.textContentType; + } + } + + public IContentType PlaintextContentType + { + get + { + if (this.plaintextContentType == null) + { + // it's OK to evaluate this more than once, and the assignment is atomic, so we don't protect this with a lock + this.plaintextContentType = _contentTypeRegistryService.GetContentType("plaintext"); + } + return this.plaintextContentType; + } + } + + public IContentType InertContentType + { + get + { + if (this.inertContentType == null) + { + // it's OK to evaluate this more than once, and the assignment is atomic, so we don't protect this with a lock + this.inertContentType = _contentTypeRegistryService.GetContentType("inert"); + } + return this.inertContentType; + } + } + + public IContentType ProjectionContentType + { + get + { + if (this.projectionContentType == null) + { + // it's OK to evaluate this more than once, and the assignment is atomic, so we don't protect this with a lock + this.projectionContentType = _contentTypeRegistryService.GetContentType("projection"); + } + return this.projectionContentType; + } + } + #endregion + + public ITextBuffer CreateTextBuffer() + { + return Make(TextContentType, StringRebuilder.Empty, false); + } + + public ITextBuffer CreateTextBuffer(IContentType contentType) + { + if (contentType == null) + { + throw new ArgumentNullException("contentType"); + } + return Make(contentType, StringRebuilder.Empty, false); + } + + public ITextBuffer CreateTextBuffer(string text, IContentType contentType) + { + return CreateTextBuffer(text, contentType, false); + } + + public ITextBuffer CreateTextBuffer(SnapshotSpan span, IContentType contentType) + { + if (contentType == null) + { + throw new ArgumentNullException("contentType"); + } + + StringRebuilder content = StringRebuilderFromSnapshotSpan(span); + + return Make(contentType, content, false); + } + + public ITextBuffer CreateTextBuffer(ITextImage image, IContentType contentType) + { + if (image == null) + { + throw new ArgumentNullException(nameof(image)); + } + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + + StringRebuilder content = StringRebuilder.Create(image); + + return Make(contentType, content, false); + } + + public ITextBuffer CreateTextBuffer(string text, IContentType contentType, bool spurnGroup) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + if (contentType == null) + { + throw new ArgumentNullException("contentType"); + } + return Make(contentType, StringRebuilder.Create(text), spurnGroup); + } + + public ITextBuffer CreateTextBuffer(TextReader reader, IContentType contentType, long length, string traceId) + { + if (reader == null) + { + throw new ArgumentNullException("reader"); + } + if (contentType == null) + { + throw new ArgumentNullException("contentType"); + } + if (length > int.MaxValue) + { + throw new InvalidOperationException(Strings.FileTooLarge); + } + + bool hasConsistentLineEndings; + int longestLineLength; + StringRebuilder content = TextImageLoader.Load(reader, length, traceId, out hasConsistentLineEndings, out longestLineLength); + + ITextBuffer buffer = Make(contentType, content, false); + if (!hasConsistentLineEndings) + { + // leave a sign that line endings are inconsistent. This is rather nasty but for now + // we don't want to pollute the API with this factoid + buffer.Properties.AddProperty("InconsistentLineEndings", true); + } + // leave a similar sign about the longest line in the buffer. + + return buffer; + } + + public ITextBuffer CreateTextBuffer(TextReader reader, IContentType contentType) + { + return CreateTextBuffer(reader, contentType, -1, "legacy"); + } + + internal static StringRebuilder StringRebuilderFromSnapshotAndSpan(ITextSnapshot snapshot, Span span) + { + return AppendStringRebuildersFromSnapshotAndSpan(StringRebuilder.Empty, snapshot, span); + } + + internal static StringRebuilder StringRebuilderFromSnapshotSpan(SnapshotSpan span) + { + return StringRebuilderFromSnapshotAndSpan(span.Snapshot, span.Span); + } + + internal static StringRebuilder StringRebuilderFromSnapshotSpans(IList<SnapshotSpan> sourceSpans, Span selectedSourceSpans) + { + StringRebuilder content = StringRebuilder.Empty; + for (int i = 0; (i < selectedSourceSpans.Length); ++i) + { + var span = sourceSpans[selectedSourceSpans.Start + i]; + content = AppendStringRebuildersFromSnapshotAndSpan(content, span.Snapshot, span.Span); + } + + return content; + } + + internal static StringRebuilder AppendStringRebuildersFromSnapshotAndSpan(StringRebuilder content, ITextSnapshot snapshot, Span span) + { + var baseSnapshot = snapshot as BaseSnapshot; + if (baseSnapshot != null) + { + content = content.Append(baseSnapshot.Content.GetSubText(span)); + } + else + { + // The we don't know what to do fallback. This should never be called unless someone provides a new snapshot + // implementation. + content = content.Append(snapshot.GetText(span)); + } + + return content; + } + + #region ITextImageFactoryService members + public ITextImage CreateTextImage(string text) + { + return CachingTextImage.Create(StringRebuilder.Create(text), null); + } + + public ITextImage CreateTextImage(TextReader reader, long length) + { + bool hasConsistentLineEndings; + int longestLineLength; + + return CachingTextImage.Create(TextImageLoader.Load(reader, length, string.Empty, out hasConsistentLineEndings, out longestLineLength), null); + } + + public ITextImage CreateTextImage(MemoryMappedFile source) + { + // Evil implementation (for now) that just reads the entire contents of the MMF. + // Eventually to be replaced with something along the lines of a version of the StringRebuilderForCompressedChars that uses the MMF directly. + using (var stream = source.CreateViewStream()) + { + using (var reader = new StreamReader(stream, Encoding.Unicode)) + { + return this.CreateTextImage(reader, -1); + } + } + } + #endregion + + private TextBuffer Make(IContentType contentType, StringRebuilder content, bool spurnGroup) + { + TextBuffer buffer = new TextBuffer(contentType, content, _textDifferencingSelectorService.DefaultTextDifferencingService, _guardedOperations, spurnGroup); + RaiseTextBufferCreatedEvent(buffer); + return buffer; + } + + public IProjectionBuffer CreateProjectionBuffer(IProjectionEditResolver projectionEditResolver, + IList<object> trackingSpans, + ProjectionBufferOptions options, + IContentType contentType) + { + // projectionEditResolver is allowed to be null. + if (trackingSpans == null) + { + throw new ArgumentNullException("trackingSpans"); + } + if (contentType == null) + { + throw new ArgumentNullException("contentType"); + } + IProjectionBuffer buffer = + new ProjectionBuffer(this, projectionEditResolver, contentType, trackingSpans, _differenceService, _textDifferencingSelectorService.DefaultTextDifferencingService, options, _guardedOperations); + RaiseProjectionBufferCreatedEvent(buffer); + return buffer; + } + + public IProjectionBuffer CreateProjectionBuffer(IProjectionEditResolver projectionEditResolver, + IList<object> trackingSpans, + ProjectionBufferOptions options) + { + // projectionEditResolver is allowed to be null. + if (trackingSpans == null) + { + throw new ArgumentNullException("trackingSpans"); + } + + IProjectionBuffer buffer = + new ProjectionBuffer(this, projectionEditResolver, ProjectionContentType, trackingSpans, _differenceService, _textDifferencingSelectorService.DefaultTextDifferencingService, options, _guardedOperations); + RaiseProjectionBufferCreatedEvent(buffer); + return buffer; + } + + public IElisionBuffer CreateElisionBuffer(IProjectionEditResolver projectionEditResolver, + NormalizedSnapshotSpanCollection exposedSpans, + ElisionBufferOptions options, + IContentType contentType) + { + // projectionEditResolver is allowed to be null. + if (exposedSpans == null) + { + throw new ArgumentNullException("exposedSpans"); + } + if (exposedSpans.Count == 0) + { + throw new ArgumentOutOfRangeException("exposedSpans"); // really? + } + if (contentType == null) + { + throw new ArgumentNullException("contentType"); + } + + if (exposedSpans[0].Snapshot != exposedSpans[0].Snapshot.TextBuffer.CurrentSnapshot) + { + // TODO: + // build against given snapshot and then move forward if necessary? + throw new ArgumentException("Elision buffer must be created against the current snapshot of its source buffer"); + } + + IElisionBuffer buffer = new ElisionBuffer(projectionEditResolver, contentType, exposedSpans[0].Snapshot.TextBuffer, + exposedSpans, options, _textDifferencingSelectorService.DefaultTextDifferencingService, _guardedOperations); + RaiseProjectionBufferCreatedEvent(buffer); + return buffer; + } + + public IElisionBuffer CreateElisionBuffer(IProjectionEditResolver projectionEditResolver, + NormalizedSnapshotSpanCollection exposedSpans, + ElisionBufferOptions options) + { + return CreateElisionBuffer(projectionEditResolver, exposedSpans, options, ProjectionContentType); + } + + public event EventHandler<TextBufferCreatedEventArgs> TextBufferCreated; + public event EventHandler<TextBufferCreatedEventArgs> ProjectionBufferCreated; + + private void RaiseTextBufferCreatedEvent(ITextBuffer buffer) + { + EventHandler<TextBufferCreatedEventArgs> handler = TextBufferCreated; + if (handler != null) + { + handler(this, new TextBufferCreatedEventArgs(buffer)); + } + } + + private void RaiseProjectionBufferCreatedEvent(IProjectionBufferBase buffer) + { + EventHandler<TextBufferCreatedEventArgs> handler = ProjectionBufferCreated; + if (handler != null) + { + handler(this, new TextBufferCreatedEventArgs(buffer)); + } + } + } +} diff --git a/src/Text/Impl/TextModel/BufferGroup.cs b/src/Text/Impl/TextModel/BufferGroup.cs new file mode 100644 index 0000000..789b3de --- /dev/null +++ b/src/Text/Impl/TextModel/BufferGroup.cs @@ -0,0 +1,688 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Text.Projection.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + using System; + + internal class BufferGroup + { + private static bool tracing = false; + private static bool detailedTracing = false; + private static Exception LastMasterEditException = null; + private static string LastMasterEditExceptionStackTrace = null; + + /// <summary> + /// The text buffers that are members of this group. All buffers related by projection must be in the + /// same group. + /// </summary> + private readonly HashSet<BufferWeakReference> members = new HashSet<BufferWeakReference>(); + + /// <summary> + /// This queue is used to sequence all events raised due to snapshot changes. It ensures that + /// events are always delivered in temporal order (i.e., no client is notified of snapshot N+1 before + /// every client is notified of snapshot N) and also to ensure that events related to a projection edit + /// are delivered 'bottom up' + /// </summary> + internal Queue<Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>> eventQueue = new Queue<Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>>(); + internal int depth = 0; + internal bool eventingInProgress = false; + + private class GraphEntry + { + public HashSet<IProjectionBufferBase> Targets { get; private set; } + public bool EditComplete { get; set; } + public bool Dependent { get; set; } + public GraphEntry(HashSet<IProjectionBufferBase> targets, bool editComplete, bool dependent) + { + Targets = targets; + EditComplete = editComplete; + Dependent = dependent; + } + } + + /// <summary> + /// Transient fields that are valid during a 'master edit transaction' + /// </summary> + private BaseBuffer masterBuffer; + private Dictionary<BaseBuffer, GraphEntry> graph; // map from nodes to edges + private Dictionary<ITextBuffer, ISubordinateTextEdit> buffer2EditMap; + private HashSet<BaseProjectionBuffer> pendingIndependentBuffers; + private EditOptions masterOptions; + private object masterEditTag; + + public BufferGroup(ITextBuffer member) + { + this.members.Add(new BufferWeakReference(member)); + } + + #region Membership + internal HashSet<BufferWeakReference> Members + { + get { return this.members; } + } + + public bool MembersContains(ITextBuffer buffer) + { + return this.members.Contains(new BufferWeakReference(buffer)); + } + + public void AddMember(ITextBuffer member) + { + // Police this.members by removing the expired ones + this.members.RemoveWhere((m) => m.Buffer == null); + + this.members.Add(new BufferWeakReference(member)); + } + + public void RemoveMember(ITextBuffer member) + { + // this is a very limited workaround to solve a Dev10 leak. New design for Dev11... + Debug.Assert(this.masterBuffer == null); + this.members.Remove(new BufferWeakReference(member)); + } + + public void Swallow(BufferGroup victim) + { + if (victim != this) + { + // todo: suitable checks to be sure it's OK to do this now + foreach (var memberWeakReference in victim.Members) + { + ITextBuffer member = memberWeakReference.Buffer; + if ((member != null) && this.members.Add(memberWeakReference)) + { + BaseBuffer baseMember = (BaseBuffer)member; + baseMember.group = this; + } + } + + if (victim.eventQueue.Count > 0) + { + // maybe reconsider using Queue type + var newEventQueue = new Queue<Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>>(victim.eventQueue); + while (this.eventQueue.Count > 0) + { + newEventQueue.Enqueue(this.eventQueue.Dequeue()); + } + this.eventQueue = newEventQueue; + victim.eventQueue.Clear(); + } + } + } + #endregion + + public ITextBuffer MasterBuffer + { + get { return this.masterBuffer; } + } + + public bool MasterEditInProgress + { + get { return this.masterBuffer != null; } + } + + public static bool Tracing + { + get { return BufferGroup.tracing; } + set { BufferGroup.tracing = value; } + } + + public static bool DetailedTracing + { + get { return BufferGroup.detailedTracing; } + set { BufferGroup.detailedTracing = value; } + } + + public Dictionary<ITextBuffer, ISubordinateTextEdit> BufferToEditMap + { + get + { + if (this.buffer2EditMap == null) + { + throw new InvalidOperationException(); + } + return this.buffer2EditMap; + } + } + + public ITextEdit GetEdit(BaseBuffer buffer) + { + return GetEdit(buffer, this.masterOptions); + } + + public ITextEdit GetEdit(BaseBuffer buffer, EditOptions options) + { + ISubordinateTextEdit subedit; + if (!this.buffer2EditMap.TryGetValue(buffer, out subedit)) + { + Debug.Assert(!(buffer is IProjectionBufferBase)); + subedit = buffer.CreateSubordinateEdit(options, null, this.masterEditTag); + this.buffer2EditMap.Add(buffer, subedit); + } + return (ITextEdit)subedit; + } + + [Conditional("DEBUG")] + private static void Trace(ITextBuffer buffer, string operation) + { + if (BufferGroup.tracing) + { + string tag = TextUtilities.GetTag(buffer); + if (string.IsNullOrEmpty(tag)) + { + tag = buffer.ToString(); + } + + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, "====> {0,12}: {1}", operation, tag)); + } + } + + [Conditional("DEBUG")] + private static void Trace(ISubordinateTextEdit subedit, string operation) + { + if (BufferGroup.tracing) + { + Trace(subedit.TextBuffer, operation); + Debug.WriteLine(subedit.ToString()); + } + } + + public void PerformMasterEdit(ITextBuffer buffer, ISubordinateTextEdit xedit, EditOptions options, object editTag) + { + Debug.Assert(this.MembersContains(buffer)); + if (this.masterBuffer != null) + { + // internal error + throw new InvalidOperationException("Master edit already in progress"); + } + + try + { + this.masterBuffer = (BaseBuffer)buffer; + this.masterOptions = options; + this.masterEditTag = editTag; + + this.buffer2EditMap = new Dictionary<ITextBuffer, ISubordinateTextEdit>(); + this.buffer2EditMap.Add(buffer, xedit); + + this.pendingIndependentBuffers = new HashSet<BaseProjectionBuffer>(); + + BuildGraph(); + + Trace(buffer, "Master"); + + Stack<ISubordinateTextEdit> appliedSubordinateEdits = new Stack<ISubordinateTextEdit>(); + while (this.buffer2EditMap.Count > 0) + { + // Pick an edit that has no possibility of further impact from target projection buffers. + ISubordinateTextEdit subedit = PickEdit(); + Trace(subedit, "Pre Apply"); + PopulateSourceEdits(subedit.TextBuffer); + subedit.PreApply(); // this may add more edits to buffer2EditMap + appliedSubordinateEdits.Push(subedit); + } + + Action cancelAction = () => + { + foreach (var edit in appliedSubordinateEdits) + { + edit.CancelApplication(); + } + + Debug.Assert(this.pendingIndependentBuffers.Count == 0); + Debug.Assert(this.depth == 0); + + this.graph = null; + this.buffer2EditMap = null; + this.masterBuffer = null; + this.pendingIndependentBuffers = null; + }; + + foreach (var edit in appliedSubordinateEdits) + { + if (!edit.CheckForCancellation(cancelAction)) + { + Debug.Assert(this.graph == null); + Debug.Assert(this.buffer2EditMap == null); + Debug.Assert(this.masterBuffer == null); + Debug.Assert(this.pendingIndependentBuffers == null); + Debug.Assert(this.eventQueue.Count == 0); + Debug.Assert(this.depth == 0); + Debug.Assert(!this.eventingInProgress); + return; + } + } + + // pendingIndependentBuffers currently do not get a voice in cancelation. + + // now interpret events in the reverse order + while (appliedSubordinateEdits.Count > 0) + { + ISubordinateTextEdit subedit = appliedSubordinateEdits.Pop(); + Trace(subedit.TextBuffer, "Final Apply"); + subedit.FinalApply(); // this creates the snapshot and queues/raises events...TODO: make it return raisers + } + + // move on to independent edits + while (this.pendingIndependentBuffers.Count > 0) + { + BaseProjectionBuffer projectionBuffer = PickIndependentBuffer(); + Trace(projectionBuffer, "Propagate"); + BaseBuffer.ITextEventRaiser raiser = projectionBuffer.PropagateSourceChanges(options, editTag); + this.eventQueue.Enqueue(new Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>(raiser, projectionBuffer)); + } + + this.graph = null; + this.buffer2EditMap = null; + this.masterBuffer = null; + this.pendingIndependentBuffers = null; + } + catch (Exception e) + { + BufferGroup.LastMasterEditException = e; + BufferGroup.LastMasterEditExceptionStackTrace = e.StackTrace; + throw; + } + } + + public void ScheduleIndependentEdit(BaseProjectionBuffer projectionBuffer) + { + // A projection buffer has received a content change event from one of its source buffers, but + // it has no edit in progress. This means it is an independent buffer (with respect to the master edit) + // and needs to have its event propagation scheduled. + this.pendingIndependentBuffers.Add(projectionBuffer); + } + + public void CancelIndependentEdit(BaseProjectionBuffer projectionBuffer) + { + // A buffer that thought it was independent turned out not to be so. May be called by buffers + // that are not in the pendingIndependentBuffers set. + this.pendingIndependentBuffers.Remove(projectionBuffer); + } + + private void PopulateSourceEdits(ITextBuffer buffer) + { + IProjectionBufferBase projectionBuffer = buffer as IProjectionBufferBase; + if (projectionBuffer != null) + { + // Add ITextEdit objects to the edit map for all source buffers of the current projection buffer that + // are themselves projection buffers. This is so that such buffers are not considered independent buffers + // if it happens that the master edit does not touch them directly (even though not touched directly, they + // may receive events from their source buffers that were touched directly, and so FinalApply() will need + // to be invoked on them in order to construct a snapshot that matches the last snapshots of their source buffers). + foreach (ITextBuffer sourceBuffer in projectionBuffer.SourceBuffers) + { + IProjectionBufferBase sourceProjectionBuffer = sourceBuffer as IProjectionBufferBase; + if (sourceProjectionBuffer != null) + { + BaseBuffer baseSourceBuffer = (BaseBuffer)sourceBuffer; + if (!this.buffer2EditMap.ContainsKey(baseSourceBuffer)) + { + this.buffer2EditMap.Add(sourceBuffer, (baseSourceBuffer.CreateSubordinateEdit(this.masterOptions, null, this.masterEditTag))); + } + } + } + } + } + + private ISubordinateTextEdit PickEdit() + { + // find a buffer for which we are sure that all potential subordinate edits have been generated. + + // this is an unsophisticated initial implementation. We are saved by the + // fact that real graphs are small (e.g. the typical venus graph has four buffers, + // five if outlining is involved) + + foreach (var pair in this.buffer2EditMap) + { + BaseBuffer buffer = (BaseBuffer)pair.Key; + GraphEntry g; + if (!this.graph.TryGetValue(buffer, out g)) + { + // if the buffer isn't in the graph, it is because it is being added to the graph + // as part of the current transaction (it must be the literal buffer of a projection buffer). + // We are cool to pick this buffer, which cannot possibly be affected by other edits. + this.buffer2EditMap.Remove(buffer); + return pair.Value; + } + else + { + if (InvulnerableToFutureEdits(g)) + { + g.EditComplete = true; + this.buffer2EditMap.Remove(buffer); // can do better with indexed iteration + return pair.Value; + } + } + } + + throw new InvalidOperationException("Internal error in BufferGroup.PickEdit"); + } + + private bool InvulnerableToFutureEdits(GraphEntry graphEntry) + { + foreach (IProjectionBufferBase target in graphEntry.Targets) + { + BaseBuffer baseTarget = (BaseBuffer)target; + GraphEntry targetEntry = this.graph[baseTarget]; + if (targetEntry.EditComplete) + { + continue; + } + if (InvulnerableToFutureEdits(targetEntry) && !this.buffer2EditMap.ContainsKey(baseTarget)) + { + targetEntry.EditComplete = true; + continue; + } + return false; + } + return true; + } + + private BaseProjectionBuffer PickIndependentBuffer() + { + // pick a buffer for which all source buffers have finished participating in the master edit. + // source buffer could be stable because + // 1. it is in the source closure of the master buffer -- all those edits are done by now + // 2. it isn't in the target closure of the master buffer + // 3. it has already been picked by this method + + // for the moment, we are ignoring #2, and such buffers won't get picked (resulting in an exception). + + foreach (BaseProjectionBuffer projectionBuffer in this.pendingIndependentBuffers) + { + bool ok = true; + foreach (ITextBuffer sourceBuffer in projectionBuffer.SourceBuffers) + { + if (!IsStableDuringIndependentPhase((BaseBuffer)sourceBuffer)) + { + ok = false; + break; + } + } + if (ok) + { + GraphEntry gg = this.graph[(BaseBuffer)projectionBuffer]; + gg.Dependent = true; // indicate has been chosen + this.pendingIndependentBuffers.Remove(projectionBuffer); + return projectionBuffer; + } + } + throw new InvalidOperationException("Couldn't pick an independent buffer"); + } + + private bool IsStableDuringIndependentPhase(BaseBuffer sourceBuffer) + { + BaseProjectionBuffer baseProjSourceBuffer = sourceBuffer as BaseProjectionBuffer; + if (baseProjSourceBuffer != null && this.pendingIndependentBuffers.Contains(baseProjSourceBuffer)) + { + return false; + } + else + { + GraphEntry g; + if (this.graph.TryGetValue(sourceBuffer, out g)) + { + return g.Dependent || !InTargetClosureOfBuffer(sourceBuffer, this.masterBuffer); + } + else + { + // must be a projection literal buffer + return true; + } + } + } + + private bool InTargetClosureOfBuffer(BaseBuffer candidateBuffer, BaseBuffer governingBuffer) + { + GraphEntry g = this.graph[governingBuffer]; + foreach (var target in g.Targets) + { + if (target == candidateBuffer) + { + return true; + } + if (InTargetClosureOfBuffer(candidateBuffer, (BaseBuffer)target)) + { + return true; + } + } + return false; + } + + #region Graph Construction + private void BuildGraph() + { + // Police this.members by removing the expired ones + this.members.RemoveWhere((member) => member.Buffer == null); + + this.graph = new Dictionary<BaseBuffer, GraphEntry>(this.members.Count); + foreach (BufferWeakReference bufferWeakReference in this.members) + { + var buffer = bufferWeakReference.Buffer; + if (buffer != null) //There could have been a GC between the police action above and here. + { + graph.Add(buffer, new GraphEntry(new HashSet<IProjectionBufferBase>(), false, false)); + } + } + foreach (BufferWeakReference bufferWeakReference in this.members) + { + IProjectionBufferBase p = bufferWeakReference.Buffer as IProjectionBufferBase; + if (p != null) + { + foreach (BaseBuffer source in p.SourceBuffers) + { + graph[source].Targets.Add(p); + } + } + } + MarkMasterClosure(this.masterBuffer); + } + + private void MarkMasterClosure(BaseBuffer buffer) + { + GraphEntry g = this.graph[buffer]; + if (!g.Dependent) + { + g.Dependent = true; + IProjectionBufferBase projectionBuffer = buffer as IProjectionBufferBase; + if (projectionBuffer != null) + { + foreach (BaseBuffer source in projectionBuffer.SourceBuffers) + { + MarkMasterClosure(source); + } + } + } + } + + public string DumpGraph() + { + StringBuilder sb = new StringBuilder("BufferGroup Graph"); + if (this.graph == null) + { + sb.AppendLine(" <null>"); + } + else + { + sb.AppendLine(""); + foreach (var p in this.graph) + { + sb.Append(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0,8} {1}: ", TextUtilities.GetTag(p.Key), p.Value.EditComplete ? "T" : "F")); + foreach (var target in p.Value.Targets) + { + sb.Append(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0,8},", TextUtilities.GetTag(target))); + } + sb.Append("\r\n"); + } + } + return sb.ToString(); + } + #endregion + + #region Edit and Event Management + public void BeginEdit() + { + if (depth < 0) + { + throw new System.InvalidOperationException(); + } + depth++; + } + + public void FinishEdit() + { + if (depth <= 0) + { + throw new System.InvalidOperationException(); + } + if (--depth == 0) + { + RaiseEvents(); + } + } + + public void CancelEdit() + { + // if we were being transactional (that is, if we had multi-buffer transactions), + // we would cancel events here. But we aren't. + FinishEdit(); + } + + public void EnqueueEvents(BaseBuffer.ITextEventRaiser raiser, BaseBuffer baseBuffer) + { + if (depth <= 0) + { + throw new System.InvalidOperationException(); + } + this.eventQueue.Enqueue(new Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>(raiser, baseBuffer)); + } + + public void EnqueueEvents(IEnumerable<BaseBuffer.ITextEventRaiser> raisers, BaseBuffer baseBuffer) + { + if (depth <= 0) + { + throw new System.InvalidOperationException(); + } + foreach (var raiser in raisers) + { + this.eventQueue.Enqueue(new Tuple<BaseBuffer.ITextEventRaiser, BaseBuffer>(raiser, baseBuffer)); + } + } + + private void RaiseEvents() + { + if (!this.eventingInProgress) + { + List<BaseBuffer> buffersToRaisePostChangedEvent = new List<BaseBuffer>(); + try + { + this.eventingInProgress = true; + while (this.eventQueue.Count > 0) + { + var pair = this.eventQueue.Dequeue(); + pair.Item1.RaiseEvent(pair.Item2, false); + + if (pair.Item1.HasPostEvent) + { + buffersToRaisePostChangedEvent.Add(pair.Item2); + } + } + } + finally + { + this.eventingInProgress = false; + } + + foreach (var buffer in buffersToRaisePostChangedEvent) + { + // Raise the post changed event in the same order the standard events + // were raised + buffer.RaisePostChangedEvent(); + } + } + } + #endregion + + /// <summary> + /// Helper class that lets a WeakReference play in a HashSet in much the same way that a conventional pointer would. + /// The goal here is to let the BufferGroup keep a HashSet of its member buffers without preventing the members from + /// being GC'd if everyone else has forgotten about them. + /// </summary> + internal class BufferWeakReference + { + private readonly WeakReference<BaseBuffer> _buffer; + private readonly int _hashCode; + + public BufferWeakReference(ITextBuffer buffer) + { + _buffer = new WeakReference<BaseBuffer>((BaseBuffer)buffer); + _hashCode = buffer.GetHashCode(); + } + + public BaseBuffer Buffer + { + get + { + BaseBuffer buffer; + if (_buffer.TryGetTarget(out buffer)) + { + return buffer; + } + + return null; + } + } + + // Override the normal equality semantics so that two instances of a BufferWeakReference for the same buffer will be "equal" + public override bool Equals(object obj) + { + if (object.ReferenceEquals(this, obj)) + { + return true; + } + + BufferWeakReference other = obj as BufferWeakReference; + if (other != null) + { + var myBuffer = this.Buffer; + + // There are two interesting scenarios where we should return true: + // We have two different BufferWeakReferences that refer to the same buffer. + // We have a dead BufferWeakReference that we are removing from the list. In this case + // we'll return true on the object.ReferenceEquals above. + // + // Given how we are using the BufferWeakReference, we will never have the situation where + // there were two distinct instances of a BufferWeakReference for the same buffer, the buffer + // died, and an equality test was done on the two dead references. + // + // As it stands, either the source buffer is alive (because we're doing a + // this.members.Remove(new BufferWeakReference(member)); + // (in which case the buffer in the WeakReference we are comparing it with is alive) or we're doing a + // this.members.RemoveWhere(b => ...) + // (in which case the ReferenceEquals test will work). + return (myBuffer == other.Buffer) && (myBuffer != null); + } + + return false; + } + + public override int GetHashCode() + { + return _hashCode; + } + } + } +} diff --git a/src/Text/Impl/TextModel/CachingTextImage.cs b/src/Text/Impl/TextModel/CachingTextImage.cs new file mode 100644 index 0000000..5df0c21 --- /dev/null +++ b/src/Text/Impl/TextModel/CachingTextImage.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + + /// <summary> + /// Helper class that improves performances of ITextImage by caching last leaf accessed for certain operations. + /// </summary> + internal class CachingTextImage : ITextImage + { + public readonly StringRebuilder Builder; + private Tuple<int, StringRebuilder> _cache; + + public static ITextImage Create(StringRebuilder builder, ITextImageVersion version) + { + return new CachingTextImage(builder, version); + } + + private CachingTextImage(StringRebuilder builder, ITextImageVersion version) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + this.Builder = builder; + this.Version = version; + } + + public ITextImageVersion Version { get; } + + public ITextImage GetSubText(Span span) { return CachingTextImage.Create(this.Builder.GetSubText(span), version: null); } + + public int Length => this.Builder.Length; + public int LineCount => (this.Builder.LineBreakCount + 1); + + public string GetText(Span span) + { + Tuple<int, StringRebuilder> cache = this.UpdateCache(span.Start); + + int offsetStart = span.Start - cache.Item1; + if (offsetStart + span.Length < cache.Item2.Length) + { + return cache.Item2.GetText(new Span(offsetStart, span.Length)); + } + + return this.Builder.GetText(span); + } + + public char[] ToCharArray(int startIndex, int length) { return this.Builder.ToCharArray(startIndex, length); } + public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) { this.Builder.CopyTo(sourceIndex, destination, destinationIndex, count); } + + public char this[int position] + { + get + { + Tuple<int, StringRebuilder> cache = this.UpdateCache(position); + + return cache.Item2[position - cache.Item1]; + } + } + + public TextImageLine GetLineFromLineNumber(int lineNumber) + { + Span extent; + int lineBreakLength; + + this.Builder.GetLineFromLineNumber(lineNumber, out extent, out lineBreakLength); + return new TextImageLine(this, lineNumber, extent, lineBreakLength); + } + + public TextImageLine GetLineFromPosition(int position) + { + return this.GetLineFromLineNumber(this.Builder.GetLineNumberFromPosition(position)); + } + + public int GetLineNumberFromPosition(int position) { return this.Builder.GetLineNumberFromPosition(position); } + public void Write(System.IO.TextWriter writer, Span span) { this.Builder.Write(writer, span); } + + private Tuple<int, StringRebuilder> UpdateCache(int position) + { + Tuple<int, StringRebuilder> cache = _cache; + if ((cache == null) || (position < cache.Item1) || (position >= (cache.Item1 + cache.Item2.Length))) + { + int offset; + StringRebuilder leaf = this.Builder.GetLeaf(position, out offset); + + cache = new Tuple<int, StringRebuilder>(offset, leaf); + + //Since cache is a class, cachedLeaf should update atomically. + _cache = cache; + } + + return cache; + } + } +} diff --git a/src/Text/Impl/TextModel/Diagrams/StringRebuilder.cd b/src/Text/Impl/TextModel/Diagrams/StringRebuilder.cd new file mode 100644 index 0000000..bd17675 --- /dev/null +++ b/src/Text/Impl/TextModel/Diagrams/StringRebuilder.cd @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1"> + <Class Name="Microsoft.VisualStudio.Text.Implementation.BaseStringRebuilder"> + <Position X="3.5" Y="0.5" Width="2.25" /> + <Members> + <Method Name="Assemble" Hidden="true" /> + <Method Name="CompareTo" Hidden="true" /> + <Method Name="CopyTo" Hidden="true" /> + <Method Name="Delete" Hidden="true" /> + <Method Name="Equals" Hidden="true" /> + <Method Name="GetEnumerator" Hidden="true" /> + <Method Name="GetLineFromLineNumber" Hidden="true" /> + <Method Name="GetLineNumberFromPosition" Hidden="true" /> + <Method Name="GetText" Hidden="true" /> + <Method Name="IEnumerable.GetEnumerator" Hidden="true" /> + <Method Name="Insert" Hidden="true" /> + <Property Name="Length" Hidden="true" /> + <Property Name="LineBreakCount" Hidden="true" /> + <Method Name="Replace" Hidden="true" /> + <Method Name="Substring" Hidden="true" /> + <Property Name="this" Hidden="true" /> + <Method Name="ToCharArray" Hidden="true" /> + <Method Name="Write" Hidden="true" /> + </Members> + <NestedTypes> + <Class Name="Microsoft.VisualStudio.Text.Implementation.BaseStringRebuilder.RebuilderEnumerator" Collapsed="true"> + <TypeIdentifier> + <NewMemberFileName>StringRebuilder\BaseStringRebuilder.cs</NewMemberFileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + </NestedTypes> + <TypeIdentifier> + <HashCode>AAAAAMAAAEAAABQJiAAAAABAAAEAAAQECAAAAAEAKCA=</HashCode> + <FileName>StringRebuilder\BaseStringRebuilder.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Microsoft.VisualStudio.Text.Implementation.BinaryStringRebuilder"> + <Position X="1.5" Y="2.5" Width="2.25" /> + <Members> + <Field Name="_crlf" Hidden="true" /> + <Field Name="_depth" Hidden="true" /> + <Field Name="_left" Hidden="true" /> + <Field Name="_length" Hidden="true" /> + <Field Name="_lineBreakCount" Hidden="true" /> + <Field Name="_maxCharactersToConsolidate" Hidden="true" /> + <Field Name="_maxLinesToConsolidate" Hidden="true" /> + <Field Name="_right" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Nested Types" Collapsed="false" /> + </Compartments> + <NestedTypes> + <Class Name="Microsoft.VisualStudio.Text.Implementation.BinaryStringRebuilder.SimpleTreeNode" Collapsed="true"> + <TypeIdentifier> + <NewMemberFileName>StringRebuilder\BinaryStringRebuilder.cs</NewMemberFileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + </NestedTypes> + <TypeIdentifier> + <HashCode>AAAMAAAAAEQAGMQNCAgAAABAACAAAUwgBAAAAAEAKAA=</HashCode> + <FileName>StringRebuilder\BinaryStringRebuilder.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Microsoft.VisualStudio.Text.Implementation.SimpleStringRebuilder"> + <Position X="5" Y="2.5" Width="2.75" /> + <Members> + <Field Name="_empty" Hidden="true" /> + <Field Name="_lineBreakSpans" Hidden="true" /> + <Field Name="_source" Hidden="true" /> + <Field Name="_span" Hidden="true" /> + </Members> + <Compartments> + <Compartment Name="Nested Types" Collapsed="false" /> + </Compartments> + <NestedTypes> + <Class Name="Microsoft.VisualStudio.Text.Implementation.SimpleStringRebuilder.LineBreak" Collapsed="true"> + <TypeIdentifier> + <NewMemberFileName>StringRebuilder\SimpleStringRebuilder.cs</NewMemberFileName> + </TypeIdentifier> + </Class> + </NestedTypes> + <TypeIdentifier> + <HashCode>AAAAAAAAAEAACMQNCAABAABAAAAAAEQgACAAAAEAKAA=</HashCode> + <FileName>StringRebuilder\SimpleStringRebuilder.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Interface Name="Microsoft.VisualStudio.Text.Implementation.IStringRebuilder"> + <Position X="8.5" Y="0.5" Width="2.25" /> + <TypeIdentifier> + <HashCode>AAAAAMAAAEAAAAQJCAAAAABAAAEAAAQAAAAAAAEAKCA=</HashCode> + <FileName>StringRebuilder\IStringRebuilder.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="Microsoft.VisualStudio.Text.Implementation.ITreeNode"> + <Position X="11.5" Y="0.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAACMAAAAAAAAAAAAAAAAAgAAAAAAAAAAA=</HashCode> + <FileName>StringRebuilder\ITreeNode.cs</FileName> + </TypeIdentifier> + </Interface> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram>
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/EncodedStreamReader.cs b/src/Text/Impl/TextModel/EncodedStreamReader.cs new file mode 100644 index 0000000..b11a34b --- /dev/null +++ b/src/Text/Impl/TextModel/EncodedStreamReader.cs @@ -0,0 +1,134 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.VisualStudio.Text.Utilities; + + internal class EncodedStreamReader + { + /// <summary> + /// Open a stream reader from the specified stream, using the default encoding + /// unless either byte order marks are found (specifying a different encoding) + /// or -- if no byte order marks are found -- one of the given encoding detectors + /// can deduce an appropriate encoding. + /// + /// Note that the stream passed to OpenStreamReader must support both Peek and setting + /// the position. + /// </summary> + /// <param name="stream">stream on which to open the stream reader</param> + /// <returns>The detected encoding or null.</returns> + public static Encoding DetectEncoding(Stream stream, + List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> encodingDetectorExtensions, + GuardedOperations guardedOperations) + { + if (stream == null) + throw new ArgumentNullException("stream"); + + long position = stream.Position; + + bool isStreamEmpty; + + Encoding detectedEncoding = CheckForBoM(stream, out isStreamEmpty); + + // If there was no BoM, try the detector extensions. + if (detectedEncoding == null && !isStreamEmpty) + { + detectedEncoding = SniffForEncoding(stream, encodingDetectorExtensions, guardedOperations); + + //Rewind the stream. + stream.Position = position; + } + + return detectedEncoding; + } + + internal static Encoding CheckForBoM(Stream stream, out bool isStreamEmpty) + { + long position = stream.Position; + + //Open a stream reader to check for byte order marks + //(we can't use an encoding that has byte order marks as the encoding for deciding whether or not). + using (StreamReader reader = new NonStreamClosingStreamReader(stream, Encoding.ASCII, detectEncodingFromByteOrderMarks: true)) + { + //We need to peek in order to force the stream reader to actually + //get the encoding. + + //Ah, except that there is a bug in the handling of the utf-32be encoding and peek. + //(if you peek you get the byte order mark and a subsequent read will also get the byte order + //mark). Read does not have this problem. If peek starts working reliably, we can go back to + //using it and not have to recreate the StreamReader when we detect byte order marks. + int peekedChar = reader.Read(); + + isStreamEmpty = peekedChar == -1; + + //Rewind the stream. + stream.Position = position; + + if (reader.CurrentEncoding == Encoding.ASCII) + { + //No byte order marks were found. + return null; + } + else + { + System.Diagnostics.Debug.Assert(reader.CurrentEncoding.GetPreamble().Length > 0); + return reader.CurrentEncoding; + } + } + } + + /// <summary> + /// Class to act as a stream reader, but that doesn't close the stream on dispose. + /// </summary> + internal class NonStreamClosingStreamReader : StreamReader + { + internal NonStreamClosingStreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks) + : base(stream, encoding, detectEncodingFromByteOrderMarks) + { } + + protected override void Dispose(bool disposing) + { + // Force the base to not dispose the stream this reader was created with since it doesn't own it. + base.Dispose(false); + } + } + + private static Encoding SniffForEncoding(Stream stream, List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> orderedEncodingDetectors, GuardedOperations guardedOperations) + { + long position = stream.Position; + + foreach (Lazy<IEncodingDetector, IEncodingDetectorMetadata> sniffer in orderedEncodingDetectors) + { + Encoding encoding = null; + try + { + encoding = sniffer.Value.GetStreamEncoding(stream); + } + catch (Exception e) + { + guardedOperations.HandleException(sniffer, e); + } + + //Rewind the stream + stream.Position = position; + + //Return if we smelled something. + if (encoding != null) + return encoding; + } + + return null; + } + + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/ExtendedCharacterDetectionDecoder.cs b/src/Text/Impl/TextModel/ExtendedCharacterDetectionDecoder.cs new file mode 100644 index 0000000..dccc8bf --- /dev/null +++ b/src/Text/Impl/TextModel/ExtendedCharacterDetectionDecoder.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Text; + + /// <summary> + /// Decoder that detects non-ASCII characters. + /// </summary> + class ExtendedCharacterDetectionDecoder : Decoder + { + private Decoder decoder; + private Action response; + + internal ExtendedCharacterDetectionDecoder(Decoder decoder, Action response) + { + this.decoder = decoder; + this.response = response; + } + + public override int GetCharCount(byte[] bytes, int index, int count) + { + return decoder.GetCharCount(bytes, index, count); + } + + public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) + { + int charCount = decoder.GetChars(bytes, byteIndex, byteCount, chars, charIndex); + if (response != null) + { + int maxCharIndex = charIndex + charCount; + for (int i = charIndex; i < maxCharIndex; i++) + { + if (chars[i] > 0x7F) + { + response(); + response = null; + break; + } + } + } + return charCount; + } + } +} diff --git a/src/Text/Impl/TextModel/ExtendedCharacterDetector.cs b/src/Text/Impl/TextModel/ExtendedCharacterDetector.cs new file mode 100644 index 0000000..dce88db --- /dev/null +++ b/src/Text/Impl/TextModel/ExtendedCharacterDetector.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System.Text; + + /// <summary> + /// Corresponds to UTF8-no-BOM encoding. + /// Determines whether multi-byte characters were decoded. + /// Throws on invalid bytes. + /// </summary> + internal class ExtendedCharacterDetector : UTF8Encoding + { + internal bool DecodedExtendedCharacters { get; private set; } + + internal ExtendedCharacterDetector() + : base(false, true) + { + DecodedExtendedCharacters = false; + } + + public override Decoder GetDecoder() + { + return new ExtendedCharacterDetectionDecoder(base.GetDecoder(), HandleExtendedCharacter); + } + + private void HandleExtendedCharacter() + { + DecodedExtendedCharacters = true; + } + } +} diff --git a/src/Text/Impl/TextModel/FallbackDetector.cs b/src/Text/Impl/TextModel/FallbackDetector.cs new file mode 100644 index 0000000..5753980 --- /dev/null +++ b/src/Text/Impl/TextModel/FallbackDetector.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + // The .NET Encoding DecoderFallbacks do not allow one to easily determine + // whether a fallback actually occurs so this type overrides whatever is necessary + // to detect it. + internal class FallbackDetector : DecoderFallback + { + private DecoderFallback decoderFallback; + + internal bool FallbackOccurred { get; private set; } + + public FallbackDetector(DecoderFallback decoderFallback) + { + this.decoderFallback = decoderFallback; + } + + public override DecoderFallbackBuffer CreateFallbackBuffer() + { + var buffer = new FallbackBufferDetector(this.decoderFallback.CreateFallbackBuffer()); + buffer.FallbackOccurred += (s, e) => this.FallbackOccurred = true; + return buffer; + } + + public override int MaxCharCount + { + get { return this.decoderFallback.MaxCharCount; } + } + + private class FallbackBufferDetector : DecoderFallbackBuffer + { + private DecoderFallbackBuffer inner; + + internal event EventHandler FallbackOccurred; + + internal FallbackBufferDetector(DecoderFallbackBuffer inner) + { + this.inner = inner; + } + + public override bool Fallback(byte[] bytesUnknown, int index) + { + if (this.FallbackOccurred != null) + this.FallbackOccurred(this, EventArgs.Empty); + + return this.inner.Fallback(bytesUnknown, index); + } + + public override char GetNextChar() + { + return this.inner.GetNextChar(); + } + + public override bool MovePrevious() + { + return this.inner.MovePrevious(); + } + + public override int Remaining + { + get { return this.inner.Remaining; } + } + } + } +} diff --git a/src/Text/Impl/TextModel/FileUtilities.cs b/src/Text/Impl/TextModel/FileUtilities.cs new file mode 100644 index 0000000..2c567c3 --- /dev/null +++ b/src/Text/Impl/TextModel/FileUtilities.cs @@ -0,0 +1,257 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal class FileUtilities + { + public static void SaveSnapshot(ITextSnapshot snapshot, + FileMode fileMode, + Encoding encoding, + string filePath) + { + Debug.Assert((fileMode == FileMode.Create) || (fileMode == FileMode.CreateNew)); + + //Save the contents of the text buffer to disk. + + string temporaryFilePath = null; + try + { + FileStream originalFileStream = null; + FileStream temporaryFileStream = FileUtilities.CreateFileStream(filePath, fileMode, out temporaryFilePath, out originalFileStream); + if (originalFileStream == null) + { + //The "normal" scenario: save the snapshot directly to disk. Either: + // there are no hard links to the target file so we can write the snapshot to the temporary and use File.Replace. + // we're creating a new file (in which case, temporaryFileStream is a misnomer: it is the stream for the file we are creating). + try + { + using (StreamWriter streamWriter = new StreamWriter(temporaryFileStream, encoding)) + { + snapshot.Write(streamWriter); + } + } + finally + { + //This is somewhat redundant: disposing of streamWriter had the side-effect of disposing of temporaryFileStream + temporaryFileStream.Dispose(); + temporaryFileStream = null; + } + + if (temporaryFilePath != null) + { + //We were saving to the original file and already have a copy of the file on disk. + int remainingAttempts = 3; + do + { + try + { + //Replace the contents of filePath with the contents of the temporary using File.Replace to + //preserve the various attributes of the original file. + File.Replace(temporaryFilePath, filePath, null, true); + temporaryFilePath = null; + + return; + } + catch (FileNotFoundException) + { + // The target file doesn't exist (someone deleted it after we detected it earlier). + // This is an acceptable condition so don't throw. + File.Move(temporaryFilePath, filePath); + temporaryFilePath = null; + + return; + } + catch (IOException) + { + //There was some other exception when trying to replace the contents of the file + //(probably because some other process had the file locked). + //Wait a few ms and try again. + System.Threading.Thread.Sleep(5); + } + } + while (--remainingAttempts > 0); + + //We're giving up on replacing the file. Try overwriting it directly (this is essentially the old Dev11 behavior). + //Do not try approach we are using for hard links (copying the original & restoring it if there is a failure) since + //getting here implies something strange is going on with the file system (Git or the like locking files) so we + //want the simplest possible fallback. + + //Failing here causes the exception to be passed to the calling code. + using (FileStream stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read)) + { + using (StreamWriter streamWriter = new StreamWriter(stream, encoding)) + { + snapshot.Write(streamWriter); + } + } + } + } + else + { + //filePath has hard links so we need to use a different approach to save the file: + // copy the original file to the temporary + // write directly to the original + // restore the original in the event of errors (which could be encoding errors and not disk issues) if there's a problem. + try + { + // Copy the contents of the original file to the temporary. + originalFileStream.CopyTo(temporaryFileStream); + + //We've got a clean copy, try writing the snapshot directly to the original file + try + { + originalFileStream.Seek(0, SeekOrigin.Begin); + originalFileStream.SetLength(0); + + //Make sure the StreamWriter is flagged leaveOpen == true. Otherwise disposing of the StreamWriter will dispose of originalFileStream and we need to + //leave originalFileStream open so we can use it to restore the original from the temporary copy we made. + using (var streamWriter = new StreamWriter(originalFileStream, encoding, bufferSize: 1024, leaveOpen: true)) //1024 == the default buffer size for a StreamWriter. + { + snapshot.Write(streamWriter); + } + } + catch + { + //Restore the original from the temporary copy we made (but rethrow the original exception since we didn't save the file). + temporaryFileStream.Seek(0, SeekOrigin.Begin); + + originalFileStream.Seek(0, SeekOrigin.Begin); + originalFileStream.SetLength(0); + + temporaryFileStream.CopyTo(originalFileStream); + + throw; + } + } + finally + { + originalFileStream.Dispose(); + originalFileStream = null; + + temporaryFileStream.Dispose(); + temporaryFileStream = null; + } + } + } + finally + { + if (temporaryFilePath != null) + { + try + { + //We do not need the temporary any longer. + if (File.Exists(temporaryFilePath)) + { + File.Delete(temporaryFilePath); + } + } + catch + { + //Failing to clean up the temporary is an ignorable exception. + } + } + } + } + + private static FileStream CreateFileStream(string filePath, FileMode fileMode, out string temporaryPath, out FileStream originalFileStream) + { + originalFileStream = null; + + if (File.Exists(filePath)) + { + // We're writing to a file that already exists. This is an error if we're trying to do a CreateNew. + if (fileMode == FileMode.CreateNew) + { + throw new IOException(filePath + " exists"); + } + + try + { + originalFileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + + //Even thoug SafeFileHandle is an IDisposable, we don't dispose of it since that closes the strem. + var safeHandle = originalFileStream.SafeFileHandle; + if (!(safeHandle.IsClosed || safeHandle.IsInvalid)) + { + BY_HANDLE_FILE_INFORMATION fi; + if (GetFileInformationByHandle(safeHandle, out fi)) + { + if (fi.NumberOfLinks <= 1) + { + // The file we're trying to write to doesn't have any hard links ... clear out the originalFileStream + // as a clue. + originalFileStream.Dispose(); + originalFileStream = null; + } + } + } + } + catch + { + if (originalFileStream != null) + { + originalFileStream.Dispose(); + originalFileStream = null; + } + + //We were not able to determine whether or not the file had hard links so throw here (aborting the save) + //since we don't know how to do it safely. + throw; + } + + string root = Path.GetDirectoryName(filePath); + + int count = 0; + while (++count < 20) + { + try + { + temporaryPath = Path.Combine(root, Path.GetRandomFileName() + "~"); //The ~ suffix hides the temporary file from GIT. + return new FileStream(temporaryPath, FileMode.CreateNew, (originalFileStream != null) ? FileAccess.ReadWrite : FileAccess.Write, FileShare.None); + } + catch (IOException) + { + //Ignore IOExceptions ... GetRandomFileName() came up with a duplicate so we need to try again. + } + } + + Debug.Fail("Unable to create a temporary file"); + } + + temporaryPath = null; + return new FileStream(filePath, fileMode, FileAccess.Write, FileShare.Read); + } + + [StructLayout(LayoutKind.Sequential)] + struct BY_HANDLE_FILE_INFORMATION + { + public uint FileAttributes; + public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime; + public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime; + public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime; + public uint VolumeSerialNumber; + public uint FileSizeHigh; + public uint FileSizeLow; + public uint NumberOfLinks; + public uint FileIndexHigh; + public uint FileIndexLow; + } + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool GetFileInformationByHandle( + Microsoft.Win32.SafeHandles.SafeFileHandle hFile, + out BY_HANDLE_FILE_INFORMATION lpFileInformation + ); + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs b/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs new file mode 100644 index 0000000..4b8f0c2 --- /dev/null +++ b/src/Text/Impl/TextModel/ForwardFidelityCustomTrackingSpan.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + + internal sealed class ForwardFidelityCustomTrackingSpan : ForwardFidelityTrackingSpan + { + object customState; + CustomTrackToVersion behavior; + + public ForwardFidelityCustomTrackingSpan(TextVersion version, Span span, object customState, CustomTrackToVersion behavior) + : base(version, span, SpanTrackingMode.Custom) + { + if (behavior == null) + { + throw new ArgumentNullException("behavior"); + } + this.behavior = behavior; + this.customState = customState; + } + + protected override Span TrackSpanForwardInTime(Span span, ITextVersion currentVersion, ITextVersion targetVersion) + { + return behavior(this, currentVersion, targetVersion, span, this.customState); + } + + protected override Span TrackSpanBackwardInTime(Span span, ITextVersion currentVersion, ITextVersion targetVersion) + { + return behavior(this, currentVersion, targetVersion, span, this.customState); + } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/ForwardFidelityTrackingPoint.cs b/src/Text/Impl/TextModel/ForwardFidelityTrackingPoint.cs new file mode 100644 index 0000000..5cbda46 --- /dev/null +++ b/src/Text/Impl/TextModel/ForwardFidelityTrackingPoint.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + /// <summary> + /// Implementation of ITrackingPoint for the Forward TrackingFidelityMode. + /// Results of moving backwards in version space are not guaranteed to match results + /// for the same version when moving forward (i.e., no special support for noninvertible transitions). + /// No special support for Undo/Redo. + /// </summary> + internal class ForwardFidelityTrackingPoint : TrackingPoint + { + #region State and Construction + private class VersionPosition + { + private ITextVersion version; + private int position; + + public VersionPosition(ITextVersion version, int position) + { + this.version = version; + this.position = position; + } + + public ITextVersion Version { get { return this.version; } } + public int Position { get { return this.position; } } + } + + private VersionPosition cachedPosition; + + public ForwardFidelityTrackingPoint(ITextVersion version, int position, PointTrackingMode trackingMode) + : base(version, position, trackingMode) + { + this.cachedPosition = new VersionPosition(version, position); + } + #endregion + + #region Overridden methods + public override ITextBuffer TextBuffer + { + get { return this.cachedPosition.Version.TextBuffer; } + } + + public override TrackingFidelityMode TrackingFidelity + { + get { return TrackingFidelityMode.Forward; } + } + + protected override int TrackPosition(ITextVersion targetVersion) + { + // Compute the new position on the requested snapshot. + // + // This method can be called simultaneously from multiple threads, and must be fast. + // + // We are relying on the atomicity of pointer copies (this.cachedPosition might change after we've + // fetched it but we will always get a self-consistent VersionPosition). This ensures we + // have proper behavior when called from multiple threads--multiple threads may all track and update the + // cached value if called at inconvenient times, but they will return consistent results. + // ForwardFidelity points do not support tracking backward, so consistency is not guaranteed in that case. + + VersionPosition cached = this.cachedPosition; + int targetPosition; + if (targetVersion == cached.Version) + { + targetPosition = cached.Position; + } + else if (targetVersion.VersionNumber > cached.Version.VersionNumber) + { + // Roll the cached version forward to the requested version. + targetPosition = Tracking.TrackPositionForwardInTime(this.trackingMode, cached.Position, cached.Version, targetVersion); + + // Cache new cached version. + this.cachedPosition = new VersionPosition(targetVersion, targetPosition); + } + else + { + // Roll backwards from the cached version. + targetPosition = Tracking.TrackPositionBackwardInTime(this.trackingMode, cached.Position, cached.Version, targetVersion); + } + return targetPosition; + } + #endregion + + #region Diagnostic Support + public override string ToString() + { + VersionPosition c = this.cachedPosition; + return ToString(c.Version, c.Position, this.trackingMode); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/ForwardFidelityTrackingSpan.cs b/src/Text/Impl/TextModel/ForwardFidelityTrackingSpan.cs new file mode 100644 index 0000000..2781cba --- /dev/null +++ b/src/Text/Impl/TextModel/ForwardFidelityTrackingSpan.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + /// <summary> + /// Implementation of ITrackingSpan for the Forward TrackingFidelityMode. + /// Results of moving backwards in version space are not guaranteed to match results + /// for the same version when moving forward (i.e., no special support for noninvertible transitions). + /// No special support for Undo/Redo. + /// </summary> + internal class ForwardFidelityTrackingSpan : TrackingSpan + { + #region State and Construction + private class VersionSpan + { + private ITextVersion version; + private Span span; + + public VersionSpan(ITextVersion version, Span span) + { + this.version = version; + this.span = span; + } + + public ITextVersion Version { get { return this.version; } } + public Span Span { get { return this.span; } } + } + + private VersionSpan cachedSpan; + + public ForwardFidelityTrackingSpan(ITextVersion version, Span span, SpanTrackingMode trackingMode) + : base(version, span, trackingMode) + { + this.cachedSpan = new VersionSpan(version, span); + } + #endregion + + #region ITrackingSpan members + public override ITextBuffer TextBuffer + { + get { return this.cachedSpan.Version.TextBuffer; } + } + + public override TrackingFidelityMode TrackingFidelity + { + get { return TrackingFidelityMode.Forward; } + } + + protected override Span TrackSpan(ITextVersion targetVersion) + { + // Compute the new span on the requested snapshot. + // + // This method can be called simultaneously from multiple threads, and must be fast. + // + // We are relying on the atomicity of pointer copies (this.cachedSpan might change after we've + // fetched it but we will always get a self-consistent VersionPosition). This ensures we + // have proper behavior when called from multiple threads--multiple threads may all track and update the + // cached value if called at inconvenient times, but they will return consistent results. + // ForwardFidelity spans do not support tracking backward, so consistency is not guaranteed in that case. + + VersionSpan cached = this.cachedSpan; + Span targetSpan; + if (targetVersion == cached.Version) + { + targetSpan = cached.Span; + } + else if (targetVersion.VersionNumber > cached.Version.VersionNumber) + { + // Compute the target span by going forward from the cached version + targetSpan = TrackSpanForwardInTime(cached.Span, cached.Version, targetVersion); + + // Update the cached value + this.cachedSpan = new VersionSpan(targetVersion, targetSpan); + } + else + { + // Roll backwards from the cached version. + targetSpan = TrackSpanBackwardInTime(cached.Span, cached.Version, targetVersion); + } + return targetSpan; + } + #endregion + + #region Helpers + protected virtual Span TrackSpanForwardInTime(Span span, ITextVersion currentVersion, ITextVersion targetVersion) + { + return Tracking.TrackSpanForwardInTime(this.trackingMode, span, currentVersion, targetVersion); + } + + /// <summary> + /// Backward mapping. Used by all fidelity modes for mapping backwards under various circumstances. + /// </summary> + protected virtual Span TrackSpanBackwardInTime(Span span, ITextVersion currentVersion, ITextVersion targetVersion) + { + return Tracking.TrackSpanBackwardInTime(this.trackingMode, span, currentVersion, targetVersion); + } + #endregion + + #region Diagnostic Support + public override string ToString() + { + VersionSpan c = this.cachedSpan; + return ToString(c.Version, c.Span, this.trackingMode); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs b/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs new file mode 100644 index 0000000..db784fb --- /dev/null +++ b/src/Text/Impl/TextModel/HighFidelityTrackingPoint.cs @@ -0,0 +1,425 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + + /// <summary> + /// Implementation of ITrackingPoint for the UndoRedo and Backward TrackingFidelityMode. + /// Records noninvertible transition and consults these records as appropriate. + /// </summary> + internal class HighFidelityTrackingPoint : TrackingPoint + { + #region State and Construction + private class VersionPositionHistory + { + private readonly ITextVersion version; + private readonly int position; + private readonly List<VersionNumberPosition> noninvertibleHistory; + + public VersionPositionHistory(ITextVersion version, int position, List<VersionNumberPosition> noninvertibleHistory) + { + this.version = version; + this.position = position; + this.noninvertibleHistory = noninvertibleHistory; + } + + public ITextVersion Version { get { return this.version; } } + public int Position { get { return this.position; } } + public List<VersionNumberPosition> NoninvertibleHistory { get { return this.noninvertibleHistory; } } + } + + private VersionPositionHistory cachedPosition; + private TrackingFidelityMode fidelity; + + internal HighFidelityTrackingPoint(ITextVersion version, int position, PointTrackingMode trackingMode, TrackingFidelityMode fidelity) + : base(version, position, trackingMode) + { + if (fidelity != TrackingFidelityMode.UndoRedo && fidelity != TrackingFidelityMode.Backward) + { + throw new ArgumentOutOfRangeException("fidelity"); + } + List<VersionNumberPosition> initialHistory = null; + if (fidelity == TrackingFidelityMode.UndoRedo && version.VersionNumber > 0) + { + // The system may perform undo operations that reach prior to the initial version; if any of + // those transitions are noninvertible, then redoing back to the initial version will give the + // wrong answer. Thus we save the state of the point for the initial version, unless the + // initial version happens to be version zero (in which case we could not undo past it). + + initialHistory = new List<VersionNumberPosition>(); + + if (version.VersionNumber != version.ReiteratedVersionNumber) + { + Debug.Assert(version.ReiteratedVersionNumber < version.VersionNumber); + // If the current version and reiterated version differ, also remember the position + // using the reiterated version number, since when undoing back to this point it + // will be the key that is used. + initialHistory.Add(new VersionNumberPosition(version.ReiteratedVersionNumber, position)); + } + + initialHistory.Add(new VersionNumberPosition(version.VersionNumber, position)); + } + this.cachedPosition = new VersionPositionHistory(version, position, initialHistory); + this.fidelity = fidelity; + } + #endregion + + #region Overrides + public override ITextBuffer TextBuffer + { + get { return this.cachedPosition.Version.TextBuffer; } + } + + public override TrackingFidelityMode TrackingFidelity + { + get { return this.fidelity; } + } + + protected override int TrackPosition(ITextVersion targetVersion) + { + // Compute the new position on the requested snapshot. + // This object caches the most recently requested version and the position in that version. + // + // We are relying on the atomicity of pointer copies (this.cachedPosition might change after we've + // fetched it below but we will always get a self-consistent VersionPosition). This ensures we + // have proper behavior when called from multiple threads (multiple threads may all track and update the + // cached value if called at inconvenient times, but they will return consistent results). + // + // In most cases, one can track backwards from the cached version to a previously computed + // version and get the same result, but this is not always the case: in particular, when the + // position lies in a deleted region, simulating reinsertion of that region will not cause + // the previous value of the position to be recovered. Such transitions are called noninvertible. + // This class explicitly tracks the positions of the point for versions for which the subsequent + // transition is noninvertible; this allows the value to be computed properly when tracking backwards + // or in undo/redo situations. + + VersionPositionHistory cached = this.cachedPosition; // must fetch just once + if (targetVersion == cached.Version) + { + // easy! + return cached.Position; + } + + List<VersionNumberPosition> noninvertibleHistory = cached.NoninvertibleHistory; + int targetPosition; + if (targetVersion.VersionNumber > cached.Version.VersionNumber) + { + // Roll the cached version forward to the requested version. + targetPosition = TrackPositionForwardInTime + (this.trackingMode, + this.fidelity, + ref noninvertibleHistory, + cached.Position, + cached.Version, + targetVersion); + + // Cache new position + this.cachedPosition = new VersionPositionHistory(targetVersion, targetPosition, noninvertibleHistory); + } + else + { + // roll backwards from the cached version + targetPosition = TrackPositionBackwardInTime + (this.trackingMode, + this.fidelity == TrackingFidelityMode.Backward ? noninvertibleHistory : null, + cached.Position, + cached.Version, + targetVersion); + } + return targetPosition; + } + #endregion + + internal static int TrackPositionForwardInTime(PointTrackingMode trackingMode, + TrackingFidelityMode fidelity, + ref List<VersionNumberPosition> noninvertibleHistory, + int currentPosition, + ITextVersion currentVersion, + ITextVersion targetVersion) + { + System.Diagnostics.Debug.Assert(targetVersion.VersionNumber > currentVersion.VersionNumber); + + // track forward in time + ITextVersion roverVersion = currentVersion; + while (roverVersion != targetVersion) + { + currentVersion = roverVersion; + roverVersion = currentVersion.Next; + + if (fidelity == TrackingFidelityMode.UndoRedo && + roverVersion.ReiteratedVersionNumber != roverVersion.VersionNumber && + noninvertibleHistory != null) + { + int p = noninvertibleHistory.BinarySearch + (new VersionNumberPosition(roverVersion.ReiteratedVersionNumber, 0), + VersionNumberPositionComparer.Instance); + if (p >= 0) + { + currentPosition = noninvertibleHistory[p].Position; + continue; + } + } + + int changeCount = currentVersion.Changes.Count; + if (changeCount == 0) + { + // A version which has no text changes preserves the reiterated version number of the previous version, + // so its version number and reiterated version number will be different. We need to record a possibly + // noninvertible change with the reiterated version number as well as with the version number. + Debug.Assert(roverVersion.VersionNumber != roverVersion.ReiteratedVersionNumber); + if (roverVersion.VersionNumber != roverVersion.ReiteratedVersionNumber) + { + RecordNoninvertibleTransition(ref noninvertibleHistory, currentPosition, roverVersion.ReiteratedVersionNumber); + } + continue; + } + + int currentVersionStartingPosition = currentPosition; + for (int c = 0; c < changeCount; ++c) + { + ITextChange textChange = currentVersion.Changes[c]; + if (IsOpaque(textChange)) + { + if (textChange.NewPosition + textChange.OldLength <= currentPosition) + { + // point is to the right of the opaque change. shift it. + currentPosition += textChange.Delta; + continue; + } + else if (textChange.NewPosition <= currentPosition) + { + // point is within the opaque change. stay put (but within the change) + if (textChange.NewEnd <= currentPosition) + { + // we shift to the left because the new text is shorter than what + // it replaced. this is a noninvertible transition, so remember it. + RecordNoninvertibleTransition(ref noninvertibleHistory, currentVersionStartingPosition, currentVersion.VersionNumber); + currentPosition = textChange.NewEnd; + } + // No further change in this version can affect us. + break; + } + else + { + // point is to the left of the opaque change. no effect, and we are done. + break; + } + } + + if (trackingMode == PointTrackingMode.Positive) + { + // Positive tracking mode: point moves with insertions at its location. + if (textChange.NewPosition <= currentPosition) + { + if (textChange.NewPosition + textChange.OldLength <= currentPosition) + { + // easy: the change is entirely to the left of our position. + // if NewPosition + OldLength == currentPosition, it means that the character + // immediately before our position was deleted (or replaced). When going back + // in time we will see this as an insertion, and since our tracking mode is positive, + // we will put ourselves after it. + currentPosition += textChange.Delta; + } + else + { + // textChange deleted text at the position of the tracked point; now the point is + // pushed to the end of the textChange insertion. + + // This is a noninvertible transition. Remember it. + RecordNoninvertibleTransition(ref noninvertibleHistory, currentVersionStartingPosition, currentVersion.VersionNumber); + currentPosition = textChange.NewEnd; + break; + } + } + else + { + // the change is entirely to the right of our position; since changes are + // sorted, we are done. + break; + } + } + else + { + // Negative tracking mode: point doesn't move with respect to insertions at its location. + if (textChange.NewPosition < currentPosition) + { + if (textChange.NewPosition + textChange.OldLength < currentPosition) + { + // easy: the change is entirely to the left of our position. Since we + // are paying attention to the tracking mode when tracking back (to handle the + // before-the-origin-version case), we need to consider NewPosition + OldLength == + // currentPosition case as noninvertible. + currentPosition += textChange.Delta; + } + else + { + // textChange deleted text at the position of the tracked point; now the point is + // at the position of the change, prior to any insertion. + + // This is a nonivertible transition. + RecordNoninvertibleTransition(ref noninvertibleHistory, currentVersionStartingPosition, currentVersion.VersionNumber); + currentPosition = textChange.NewPosition; + break; + } + } + else + { + break; + } + } + } + } + + return currentPosition; + } + + private static void RecordNoninvertibleTransition(ref List<VersionNumberPosition> noninvertibleHistory, int currentPosition, int versionNumber) + { + if (noninvertibleHistory == null) + { + noninvertibleHistory = new List<VersionNumberPosition>(); + } + if (noninvertibleHistory.Count > 0 && noninvertibleHistory[noninvertibleHistory.Count - 1].VersionNumber == versionNumber) + { + // We already recorded the version/position, either because it's the first version + // or because of a reiterated version anomaly due to a version with no text changes + Debug.Assert(currentPosition == noninvertibleHistory[noninvertibleHistory.Count - 1].Position); + return; + } + noninvertibleHistory.Add(new VersionNumberPosition(versionNumber, currentPosition)); + + // this list had better be sorted! + Debug.Assert((noninvertibleHistory.Count == 1) || + (noninvertibleHistory[noninvertibleHistory.Count - 1].VersionNumber > + noninvertibleHistory[noninvertibleHistory.Count - 2].VersionNumber)); + } + + internal static int TrackPositionBackwardInTime(PointTrackingMode trackingMode, + List<VersionNumberPosition> noninvertibleHistory, + int currentPosition, + ITextVersion currentVersion, + ITextVersion targetVersion) + { + System.Diagnostics.Debug.Assert(targetVersion.VersionNumber < currentVersion.VersionNumber); + + ITextVersion[] textVersionsStack = new ITextVersion[currentVersion.VersionNumber - targetVersion.VersionNumber]; + int top = 0; + { + ITextVersion roverVersion = targetVersion; + while (roverVersion != currentVersion) + { + textVersionsStack[top++] = roverVersion; + roverVersion = roverVersion.Next; + } + } + + while (top > 0) + { + ITextVersion textVersion = textVersionsStack[--top]; + + if (noninvertibleHistory != null) + { + int p = noninvertibleHistory.BinarySearch + (new VersionNumberPosition(textVersion.VersionNumber, 0), VersionNumberPositionComparer.Instance); + if (p >= 0) + { + // the undo or redo change was noninvertible, so we've recorded the state before the change + currentPosition = noninvertibleHistory[p].Position; + continue; + } + } + + IList<ITextChange> textChanges = textVersion.Changes; + for (int tc = textChanges.Count - 1; tc >= 0; --tc) + { + ITextChange textChange = textChanges[tc]; + if (IsOpaque(textChange)) + { + if (textChange.NewEnd <= currentPosition) + { + // point is to the right of the opaque change. shift it. + currentPosition -= textChange.Delta; + } + else if (textChange.NewPosition <= currentPosition) + { + // point is within the opaque change. stay put (but within the change) + currentPosition = Math.Min(currentPosition, textChange.NewPosition + textChange.OldLength); + } + continue; + } + + // tracking modes don't matter when going back over ground we've previously traversed, + // because we are really reversing a deletion whose behavior had nothing to do with the + // tracking mode, and the correct answer is captured in the noninvertible history. However, + // when going back in time before the tracking point was created, it makes sense to honor + // the tracking mode in the absence of any other information. + if (trackingMode == PointTrackingMode.Positive) + { + // Positive tracking mode: point moves with insertions at its location. + if (textChange.NewPosition <= currentPosition) + { + if (textChange.NewEnd <= currentPosition) + { + currentPosition -= textChange.Delta; + } + else + { + currentPosition = textChange.NewPosition + textChange.OldLength; + } + } + } + else + { + if (textChange.NewPosition < currentPosition) + { + if (textChange.NewEnd < currentPosition) + { + currentPosition -= textChange.Delta; + } + else + { + // this is why we run the changes in reverse... + currentPosition = textChange.NewPosition; + } + } + } + } + } + + return currentPosition; + } + + private static bool IsOpaque(ITextChange textChange) + { + ITextChange2 tc2 = textChange as ITextChange2; + return tc2 != null && tc2.IsOpaque; + } + + #region Diagnostic Support + public override string ToString() + { + VersionPositionHistory c = this.cachedPosition; + System.Text.StringBuilder sb = new System.Text.StringBuilder("*"); + sb.Append(ToString(c.Version, c.Position, this.trackingMode)); + if (c.NoninvertibleHistory != null) + { + sb.Append("["); + foreach (VersionNumberPosition vp in c.NoninvertibleHistory) + { + sb.Append(string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0}@{1}", vp.VersionNumber, vp.Position)); + } + sb.Append("]"); + } + return sb.ToString(); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs b/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs new file mode 100644 index 0000000..6eb6ca4 --- /dev/null +++ b/src/Text/Impl/TextModel/HighFidelityTrackingSpan.cs @@ -0,0 +1,190 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + + /// <summary> + /// Implementation of ITrackingSpan for the UndoRedo and Backward TrackingFidelityMode. + /// Records noninvertible transition and consults these records as appropriate. + /// </summary> + internal class HighFidelityTrackingSpan : TrackingSpan + { + #region State and Construction + private class VersionSpanHistory + { + private readonly ITextVersion version; + private readonly Span span; + private readonly List<VersionNumberPosition> noninvertibleStartHistory; + private readonly List<VersionNumberPosition> noninvertibleEndHistory; + + public VersionSpanHistory(ITextVersion version, + Span span, + List<VersionNumberPosition> noninvertibleStartHistory, + List<VersionNumberPosition> noninvertibleEndHistory) + { + this.version = version; + this.span = span; + this.noninvertibleStartHistory = noninvertibleStartHistory; + this.noninvertibleEndHistory = noninvertibleEndHistory; + } + + public ITextVersion Version { get { return this.version; } } + public Span Span { get { return this.span; } } + public List<VersionNumberPosition> NoninvertibleStartHistory { get { return this.noninvertibleStartHistory; } } + public List<VersionNumberPosition> NoninvertibleEndHistory { get { return this.noninvertibleEndHistory; } } + } + + private VersionSpanHistory cachedSpan; + private TrackingFidelityMode fidelity; + + internal HighFidelityTrackingSpan(ITextVersion version, Span span, SpanTrackingMode spanTrackingMode, TrackingFidelityMode fidelity) + : base(version, span, spanTrackingMode) + { + if (fidelity != TrackingFidelityMode.UndoRedo && fidelity != TrackingFidelityMode.Backward) + { + throw new ArgumentOutOfRangeException("fidelity"); + } + List<VersionNumberPosition> startHistory = null; + List<VersionNumberPosition> endHistory = null; + if (fidelity == TrackingFidelityMode.UndoRedo && version.VersionNumber > 0) + { + // The system may perform undo operations that reach prior to the initial version; if any of + // those transitions are noninvertible, then redoing back to the initial version will give the + // wrong answer. Thus we save the state of the span for the initial version, unless + // the initial version happens to be version zero (in which case we could not undo past it). + + startHistory = new List<VersionNumberPosition>(); + endHistory = new List<VersionNumberPosition>(); + + if (version.VersionNumber != version.ReiteratedVersionNumber) + { + Debug.Assert(version.ReiteratedVersionNumber < version.VersionNumber); + // If the current version and reiterated version differ, also remember the position + // using the reiterated version number, since when undoing back to this point it + // will be the key that is used. + startHistory.Add(new VersionNumberPosition(version.ReiteratedVersionNumber, span.Start)); + endHistory.Add(new VersionNumberPosition(version.ReiteratedVersionNumber, span.End)); + } + + startHistory.Add(new VersionNumberPosition(version.VersionNumber, span.Start)); + endHistory.Add(new VersionNumberPosition(version.VersionNumber, span.End)); + } + this.cachedSpan = new VersionSpanHistory(version, span, startHistory, endHistory); + this.fidelity = fidelity; + } + #endregion + + #region Overrides + public override ITextBuffer TextBuffer + { + get { return this.cachedSpan.Version.TextBuffer; } + } + + public override TrackingFidelityMode TrackingFidelity + { + get { return this.fidelity; } + } + + protected override Span TrackSpan(ITextVersion targetVersion) + { + // Compute the span on the requested snapshot. + // This object caches the most recently requested version and the span in that version. + // + // We are relying on the atomicity of pointer copies (this.cachedSpan might change after we've + // fetched it below but we will always get a self-consistent VersionSpan). This ensures we + // have proper behavior when called from multiple threads (multiple threads may all track and update the + // cached value if called at inconvenient times, but they will return consistent results). + // + // In most cases, one can track backwards from the cached version to a previously computed + // version and get the same result, but this is not always the case: in particular, when one or both + // ends of the span lie in a deleted region, simulating reinsertion of that region will not cause + // the previous value of the span to be recovered. Such transitions are called noninvertible. + // This class explicitly tracks the positions of span endpoints for versions for which the subsequent + // transition is noninvertible; this allows the value to be computed properly when tracking backwards + // or in undo/redo situations. + + VersionSpanHistory cached = this.cachedSpan; // must fetch just once + if (targetVersion == cached.Version) + { + // easy! + return cached.Span; + } + + PointTrackingMode startMode = + (this.trackingMode == SpanTrackingMode.EdgeExclusive || this.trackingMode == SpanTrackingMode.EdgePositive) + ? PointTrackingMode.Positive + : PointTrackingMode.Negative; + + PointTrackingMode endMode = + (this.trackingMode == SpanTrackingMode.EdgeExclusive || this.trackingMode == SpanTrackingMode.EdgeNegative) + ? PointTrackingMode.Negative + : PointTrackingMode.Positive; + + List<VersionNumberPosition> noninvertibleStartHistory = cached.NoninvertibleStartHistory; + List<VersionNumberPosition> noninvertibleEndHistory = cached.NoninvertibleEndHistory; + + Span targetSpan; + if (targetVersion.VersionNumber > cached.Version.VersionNumber) + { + // Compute the target span by going forward from the cached version + int start = HighFidelityTrackingPoint.TrackPositionForwardInTime + (startMode, this.fidelity, ref noninvertibleStartHistory, cached.Span.Start, cached.Version, targetVersion); + int end = HighFidelityTrackingPoint.TrackPositionForwardInTime + (endMode, this.fidelity, ref noninvertibleEndHistory, cached.Span.End, cached.Version, targetVersion); + targetSpan = Span.FromBounds(start, System.Math.Max(start, end)); + + // Cache the new span + this.cachedSpan = new VersionSpanHistory(targetVersion, targetSpan, noninvertibleStartHistory, noninvertibleEndHistory); + } + else + { + // we are looking for a version prior to the cached version. + int start = HighFidelityTrackingPoint.TrackPositionBackwardInTime + (startMode, this.fidelity == TrackingFidelityMode.Backward ? noninvertibleStartHistory : null, cached.Span.Start, cached.Version, targetVersion); + int end = HighFidelityTrackingPoint.TrackPositionBackwardInTime + (endMode, this.fidelity == TrackingFidelityMode.Backward ? noninvertibleEndHistory : null, cached.Span.End, cached.Version, targetVersion); + + targetSpan = Span.FromBounds(start, System.Math.Max(start, end)); + } + return targetSpan; + } + #endregion + + #region Diagnostic Support + + public override string ToString() + { + VersionSpanHistory c = this.cachedSpan; + System.Text.StringBuilder sb = new System.Text.StringBuilder("*"); + sb.Append(ToString(c.Version, c.Span, this.trackingMode)); + if (c.NoninvertibleStartHistory != null) + { + sb.Append("[Start"); + foreach (VersionNumberPosition vp in c.NoninvertibleStartHistory) + { + sb.Append(string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0}@{1}", vp.VersionNumber, vp.Position)); + } + sb.Append("]"); + } + if (c.NoninvertibleEndHistory != null) + { + sb.Append("[End"); + foreach (VersionNumberPosition vp in c.NoninvertibleEndHistory) + { + sb.Append(string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0}@{1}", vp.VersionNumber, vp.Position)); + } + sb.Append("]"); + } + return sb.ToString(); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/IInternalTextBufferFactory.cs b/src/Text/Impl/TextModel/IInternalTextBufferFactory.cs new file mode 100644 index 0000000..4707f90 --- /dev/null +++ b/src/Text/Impl/TextModel/IInternalTextBufferFactory.cs @@ -0,0 +1,25 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Subset of ITextBufferFactoryService. Used to avoid having to mock the asset system in unit tests. + /// </summary> + internal interface IInternalTextBufferFactory + { + ITextBuffer CreateTextBuffer(string text, IContentType contentType); + + ITextBuffer CreateTextBuffer(string text, IContentType contentType, bool spurnGroup); + + IContentType TextContentType { get; } + IContentType InertContentType { get; } + IContentType ProjectionContentType { get; } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/ISubordinateTextEdit.cs b/src/Text/Impl/TextModel/ISubordinateTextEdit.cs new file mode 100644 index 0000000..91d7236 --- /dev/null +++ b/src/Text/Impl/TextModel/ISubordinateTextEdit.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + + /// <summary> + /// These methods augment ITextEdit to support editing of source buffers of projection and elision buffers. + /// </summary> + internal interface ISubordinateTextEdit + { + /// <summary> + /// Compute effects of an edit on source buffers, if any, and add the source edits to the BufferGroup. + /// </summary> + void PreApply(); + + /// <summary> + /// Checks whether the edit on the buffer is allowed to continue. + /// </summary> + /// <param name="cancelAction">Action to perform immediately upon edit cancelation.</param> + /// <returns>True if the edit can continue.</returns> + bool CheckForCancellation(Action cancelAction); + + /// <summary> + /// Commit effects of the edit, applying them to source buffers (if any). + /// </summary> + void FinalApply(); + + /// <summary> + /// The <see cref="ITextBuffer"/> to which this edit applies. + /// </summary> + ITextBuffer TextBuffer { get; } + + /// <summary> + /// Restores any edit-in-progress state on the associated buffer. + /// </summary> + void CancelApplication(); + + /// <summary> + /// Whether the edit has been canceled. + /// </summary> + bool Canceled { get; } + + /// <summary> + /// Mark the latest change in the edit as corresponding to a particular part of a master edit. + /// </summary> + /// <param name="masterChangeOffset">The index into the master edit's inserted text + /// that corresponds to the text that's being inserted by this subordinate edit.</param> + void RecordMasterChangeOffset(int masterChangeOffset); + } +} diff --git a/src/Text/Impl/TextModel/LineBreakBoundaryConditions.cs b/src/Text/Impl/TextModel/LineBreakBoundaryConditions.cs new file mode 100644 index 0000000..26eda40 --- /dev/null +++ b/src/Text/Impl/TextModel/LineBreakBoundaryConditions.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + + /// <summary> + /// Describe the context of some text change with respect to compound line breaks. + /// </summary> + [Flags] + internal enum LineBreakBoundaryConditions : byte + { + /// <summary> + /// The change is neither preceded by a return character nor followed by a newline character. + /// </summary> + None = 0x0, + + /// <summary> + /// The change is immediately preceded by a return character. + /// </summary> + PrecedingReturn = 0x1, + + /// <summary> + /// The change is followed immediately by a newline character. + /// </summary> + SucceedingNewline = 0x2 + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/MappingPoint.cs b/src/Text/Impl/TextModel/MappingPoint.cs new file mode 100644 index 0000000..46e34ec --- /dev/null +++ b/src/Text/Impl/TextModel/MappingPoint.cs @@ -0,0 +1,153 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using Microsoft.VisualStudio.Text.Projection; + + internal partial class MappingPoint : IMappingPoint + { + SnapshotPoint anchorPoint; + PointTrackingMode trackingMode; + IBufferGraph bufferGraph; + + public MappingPoint(SnapshotPoint anchorPoint, PointTrackingMode trackingMode, IBufferGraph bufferGraph) + { + if (anchorPoint.Snapshot == null) + { + throw new ArgumentNullException("anchorPoint"); + } + if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + if (bufferGraph == null) + { + throw new ArgumentNullException("bufferGraph"); + } + this.anchorPoint = anchorPoint; + this.trackingMode = trackingMode; + this.bufferGraph = bufferGraph; + } + + public ITextBuffer AnchorBuffer + { + get { return this.anchorPoint.Snapshot.TextBuffer; } + } + + public IBufferGraph BufferGraph + { + get { return this.bufferGraph; } + } + + public SnapshotPoint? GetPoint(ITextBuffer targetBuffer, PositionAffinity affinity) + { + if (targetBuffer == null) + { + throw new ArgumentNullException("targetBuffer"); + } + ITextBuffer anchorBuffer = this.AnchorBuffer; + SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode); + if (anchorBuffer == targetBuffer) + { + return currentPoint; + } + + ITextBuffer topBuffer = this.bufferGraph.TopBuffer; + if (targetBuffer == topBuffer) + { + return this.bufferGraph.MapUpToBuffer(currentPoint, this.trackingMode, affinity, topBuffer); + } + else if (anchorBuffer == topBuffer) + { + return this.bufferGraph.MapDownToBuffer(currentPoint, this.trackingMode, targetBuffer, affinity); + } + else + { + // we don't know a priori which way to go, so we'll guess + if (anchorBuffer is IProjectionBufferBase) + { + SnapshotPoint? tentative = this.bufferGraph.MapDownToBuffer(currentPoint, this.trackingMode, targetBuffer, affinity); + if (tentative.HasValue) + { + return tentative; + } + } + // ok, go the other way + return this.bufferGraph.MapUpToBuffer(currentPoint, this.trackingMode, affinity, targetBuffer); + } + } + + public SnapshotPoint? GetPoint(ITextSnapshot targetSnapshot, PositionAffinity affinity) + { + if (targetSnapshot == null) + throw new ArgumentNullException("targetSnapshot"); + + SnapshotPoint? result = GetPoint(targetSnapshot.TextBuffer, affinity); + if (result.HasValue && (result.Value.Snapshot != targetSnapshot)) + { + result = result.Value.TranslateTo(targetSnapshot, this.trackingMode); + } + + return result; + } + + public SnapshotPoint? GetPoint(Predicate<ITextBuffer> match, PositionAffinity affinity) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + ITextBuffer anchorBuffer = this.AnchorBuffer; + SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode); + if (match(anchorBuffer)) + { + return currentPoint; + } + + if (anchorBuffer == this.bufferGraph.TopBuffer) + { + // the only way to go is down + return this.bufferGraph.MapDownToFirstMatch(currentPoint, this.trackingMode, snapshot => (match(snapshot.TextBuffer)), affinity); + } + else + { + // guess which way to go + if (anchorBuffer is IProjectionBufferBase) + { + SnapshotPoint? tentative = this.bufferGraph.MapDownToFirstMatch(currentPoint, this.trackingMode, snapshot => (match(snapshot.TextBuffer)), affinity); + if (tentative.HasValue) + { + return tentative; + } + } + // go the other way. + if (match(this.bufferGraph.TopBuffer)) + { + return this.bufferGraph.MapUpToBuffer(currentPoint, this.trackingMode, affinity, this.bufferGraph.TopBuffer); + } + else + { + return this.bufferGraph.MapUpToFirstMatch(currentPoint, this.trackingMode, snapshot => (match(snapshot.TextBuffer)), affinity); + } + } + } + + public SnapshotPoint? GetInsertionPoint(Predicate<ITextBuffer> match) + { + // always maps down + if (match == null) + { + throw new ArgumentNullException("match"); + } + ITextBuffer anchorBuffer = this.AnchorBuffer; + SnapshotPoint currentPoint = this.anchorPoint.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode); + return this.bufferGraph.MapDownToInsertionPoint(currentPoint, this.trackingMode, snapshot => (match(snapshot.TextBuffer))); + } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/MappingSpan.cs b/src/Text/Impl/TextModel/MappingSpan.cs new file mode 100644 index 0000000..a42f0fe --- /dev/null +++ b/src/Text/Impl/TextModel/MappingSpan.cs @@ -0,0 +1,162 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Text.Utilities; + + internal partial class MappingSpan : IMappingSpan + { + private SnapshotSpan anchorSpan; + private SpanTrackingMode trackingMode; + private IBufferGraph bufferGraph; + + public MappingSpan(SnapshotSpan anchorSpan, SpanTrackingMode trackingMode, IBufferGraph bufferGraph) + { + if (anchorSpan.Snapshot == null) + { + throw new ArgumentNullException("anchorSpan"); + } + if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + if (bufferGraph == null) + { + throw new ArgumentNullException("bufferGraph"); + } + this.anchorSpan = anchorSpan; + this.trackingMode = trackingMode; + this.bufferGraph = bufferGraph; + } + + public IMappingPoint Start + { + get { return new MappingPoint(new SnapshotPoint(this.anchorSpan.Snapshot, this.anchorSpan.Start), + (this.trackingMode == SpanTrackingMode.EdgeInclusive || + this.trackingMode == SpanTrackingMode.EdgeNegative) + ? PointTrackingMode.Negative + : PointTrackingMode.Positive, + this.bufferGraph); } + } + + public IMappingPoint End + { + get { return new MappingPoint(new SnapshotPoint(this.anchorSpan.Snapshot, this.anchorSpan.End), + (this.trackingMode == SpanTrackingMode.EdgeExclusive || + this.trackingMode == SpanTrackingMode.EdgeNegative) + ? PointTrackingMode.Negative + : PointTrackingMode.Positive, + this.bufferGraph); + } + } + + public ITextBuffer AnchorBuffer + { + get { return this.anchorSpan.Snapshot.TextBuffer; } + } + + public IBufferGraph BufferGraph + { + get { return this.bufferGraph; } + } + + public NormalizedSnapshotSpanCollection GetSpans(ITextBuffer targetBuffer) + { + // null textBuffer check will be handled by the buffer graph + ITextBuffer anchorBuffer = this.AnchorBuffer; + SnapshotSpan currentSpan = this.anchorSpan.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode); + + if (anchorBuffer == targetBuffer) + { + return new NormalizedSnapshotSpanCollection(currentSpan); + } + + ITextBuffer topBuffer = this.bufferGraph.TopBuffer; + if (targetBuffer == topBuffer) + { + return this.bufferGraph.MapUpToBuffer(currentSpan, this.trackingMode, topBuffer); + } + else if (anchorBuffer == topBuffer) + { + return this.bufferGraph.MapDownToBuffer(currentSpan, this.trackingMode, targetBuffer); + } + else + { + if (anchorBuffer is IProjectionBufferBase) + { + NormalizedSnapshotSpanCollection tentative = this.bufferGraph.MapDownToBuffer(currentSpan, this.trackingMode, targetBuffer); + if (tentative.Count > 0) + { + return tentative; + } + } + return this.bufferGraph.MapUpToBuffer(currentSpan, this.trackingMode, targetBuffer); + } + } + + public NormalizedSnapshotSpanCollection GetSpans(ITextSnapshot targetSnapshot) + { + if (targetSnapshot == null) + throw new ArgumentNullException("targetSnapshot"); + + NormalizedSnapshotSpanCollection results = GetSpans(targetSnapshot.TextBuffer); + if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot)) + { + FrugalList<SnapshotSpan> translatedSpans = new FrugalList<SnapshotSpan>(); + foreach (SnapshotSpan s in results) + { + translatedSpans.Add(s.TranslateTo(targetSnapshot, trackingMode)); + } + + results = new NormalizedSnapshotSpanCollection(translatedSpans); + } + + return results; + } + + public NormalizedSnapshotSpanCollection GetSpans(Predicate<ITextBuffer> match) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + + ITextBuffer anchorBuffer = this.AnchorBuffer; + SnapshotSpan currentSpan = this.anchorSpan.TranslateTo(anchorBuffer.CurrentSnapshot, this.trackingMode); + if (match(anchorBuffer)) + { + return new NormalizedSnapshotSpanCollection(currentSpan); + } + if (anchorBuffer == this.bufferGraph.TopBuffer) + { + return this.bufferGraph.MapDownToFirstMatch(currentSpan, this.trackingMode, snapshot => (match(snapshot.TextBuffer))); + } + else + { + // guess which way to go + if (anchorBuffer is IProjectionBufferBase) + { + NormalizedSnapshotSpanCollection tentative = this.bufferGraph.MapDownToFirstMatch(currentSpan, this.trackingMode, snapshot => (match(snapshot.TextBuffer))); + if (tentative.Count > 0) + { + return tentative; + } + } + return this.bufferGraph.MapUpToFirstMatch(currentSpan, this.trackingMode, snapshot => (match(snapshot.TextBuffer))); + } + } + + public override string ToString() + { + return String.Format("MappingSpan anchored at {0}", this.anchorSpan); + } + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs b/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs new file mode 100644 index 0000000..8e3352f --- /dev/null +++ b/src/Text/Impl/TextModel/NormalizedTextChangeCollection.cs @@ -0,0 +1,523 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Diagnostics; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Utilities; + + internal partial class NormalizedTextChangeCollection : INormalizedTextChangeCollection + { + public static readonly NormalizedTextChangeCollection Empty = new NormalizedTextChangeCollection(new TextChange[0]); + private readonly IReadOnlyList<TextChange> _changes; + + public static INormalizedTextChangeCollection Create(IReadOnlyList<TextChange> changes) + { + INormalizedTextChangeCollection result = GetTrivialCollection(changes); + return result != null ? result : new NormalizedTextChangeCollection(changes); + } + + public static INormalizedTextChangeCollection Create(IReadOnlyList<TextChange> changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService, + ITextSnapshot before = null, ITextSnapshot after = null) + { + INormalizedTextChangeCollection result = GetTrivialCollection(changes); + return result != null ? result : new NormalizedTextChangeCollection(changes, differenceOptions, textDifferencingService, before, after); + } + + private static INormalizedTextChangeCollection GetTrivialCollection(IReadOnlyList<TextChange> changes) + { + if (changes == null) + { + throw new ArgumentNullException("changes"); + } + + if (changes.Count == 0) + { + return NormalizedTextChangeCollection.Empty; + } + else if (changes.Count == 1) + { + TextChange tc = changes[0]; + if (tc.OldLength + tc.NewLength == 1 && tc.LineBreakBoundaryConditions == LineBreakBoundaryConditions.None && + tc.LineCountDelta == 0) + { + bool isInsertion = tc.NewLength == 1; + char data = isInsertion ? tc.NewText[0] : tc.OldText[0]; + return new TrivialNormalizedTextChangeCollection(data, isInsertion, tc.OldPosition); + } + } + + return null; + } + + /// <summary> + /// Construct a normalized version of the given TextChange collection, + /// but don't compute minimal edits. + /// </summary> + /// <param name="changes">List of changes to normalize</param> + private NormalizedTextChangeCollection(IReadOnlyList<TextChange> changes) + { + _changes = Normalize(changes, null, null, null, null); + } + + /// <summary> + /// Construct a normalized version of the given TextChange collection. + /// </summary> + /// <param name="changes">List of changes to normalize</param> + /// <param name="differenceOptions">The difference options to use for minimal differencing, if any.</param> + /// <param name="textDifferencingService">The difference service to use, if differenceOptions were supplied.</param> + /// <param name="before">Text snapshot before the change (can be null).</param> + /// <param name="after">Text snapshot after the change (can be null).</param> + private NormalizedTextChangeCollection(IReadOnlyList<TextChange> changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService, + ITextSnapshot before, ITextSnapshot after) + { + _changes = Normalize(changes, differenceOptions, textDifferencingService, before, after); + } + + public bool IncludesLineChanges + { + get + { + foreach (ITextChange change in this) + { + if (change.LineCountDelta != 0) + { + return true; + } + } + return false; + } + } + + /// <summary> + /// Normalize a sequence of changes that were all applied consecutively to the same version of a buffer. Positions of the + /// normalized changes are adjusted to account for other changes that occur at lower indexes in the + /// buffer, and changes are sorted and merged if possible. + /// </summary> + /// <param name="changes">The changes to normalize.</param> + /// <param name="differenceOptions">The options to use for minimal differencing, if any.</param> + /// <param name="before">Text snapshot before the change (can be null).</param> + /// <param name="after">Text snapshot after the change (can be null).</param> + /// <returns>A (possibly empty) list of changes, sorted by Position, with adjacent and overlapping changes combined + /// where possible.</returns> + /// <exception cref="ArgumentNullException"><paramref name="changes"/> is null.</exception> + private static IReadOnlyList<TextChange> Normalize(IReadOnlyList<TextChange> changes, StringDifferenceOptions? differenceOptions, ITextDifferencingService textDifferencingService, + ITextSnapshot before, ITextSnapshot after) + { + if (changes.Count == 1 && differenceOptions == null) + { + // By far the most common path + // If we are computing minimal changes, we need to go through the + // algorithm anyway, since this change may be split up into many + // smaller changes + return new TextChange[] { changes[0] }; + } + else if (changes.Count == 0) + { + return new TextChange[0]; + } + + TextChange[] work = TextUtilities.StableSort(changes, TextChange.Compare); + + // work is now sorted by increasing Position + + int accumulatedDelta = 0; + int a = 0; + int b = 1; + while (b < work.Length) + { + // examine a pair of changes and attempt to combine them + TextChange aChange = work[a]; + TextChange bChange = work[b]; + int gap = bChange.OldPosition - aChange.OldEnd; + + if (gap > 0) + { + // independent changes + aChange.NewPosition = aChange.OldPosition + accumulatedDelta; + accumulatedDelta += aChange.Delta; + a = b; + b = a + 1; + } + else + { + // dependent changes. merge all adjacent dependent changes into a single change in one pass, + // to avoid expensive pairwise concatenations. + // + // Use StringRebuilders (which allow strings to be concatenated without creating copies of the strings) to assemble the + // pieces. + StringRebuilder newRebuilder = aChange._newText; + StringRebuilder oldRebuilder = aChange._oldText; + + int aChangeIncrementalDeletions = 0; + do + { + newRebuilder = newRebuilder.Append(bChange._newText); + + if (gap == 0) + { + // abutting deletions + oldRebuilder = oldRebuilder.Append(bChange._oldText); + aChangeIncrementalDeletions += bChange.OldLength; + aChange.LineBreakBoundaryConditions = + (aChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) | + (bChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline); + } + else + { + // overlapping deletions + if (aChange.OldEnd + aChangeIncrementalDeletions < bChange.OldEnd) + { + int overlap = aChange.OldEnd + aChangeIncrementalDeletions - bChange.OldPosition; + oldRebuilder = oldRebuilder.Append(bChange._oldText.GetSubText(Span.FromBounds(overlap, bChange._oldText.Length))); + aChangeIncrementalDeletions += (bChange.OldLength - overlap); + aChange.LineBreakBoundaryConditions = + (aChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) | + (bChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline); + } + // else bChange deletion subsumed by aChange deletion + } + + work[b] = null; + b++; + if (b == work.Length) + { + break; + } + bChange = work[b]; + gap = bChange.OldPosition - aChange.OldEnd - aChangeIncrementalDeletions; + } while (gap <= 0); + + work[a]._oldText = oldRebuilder; + work[a]._newText = newRebuilder; + + if (b < work.Length) + { + aChange.NewPosition = aChange.OldPosition + accumulatedDelta; + accumulatedDelta += aChange.Delta; + a = b; + b = a + 1; + } + } + } + // a points to the last surviving change + work[a].NewPosition = work[a].OldPosition + accumulatedDelta; + + List<TextChange> result = new List<TextChange>(); + + if (differenceOptions.HasValue) + { + if (textDifferencingService == null) + { + throw new ArgumentNullException("stringDifferenceUtility"); + } + foreach (TextChange change in work) + { + if (change == null) continue; + + // Make sure this is a replacement + if (change.OldLength == 0 || change.NewLength == 0) + { + result.Add(change); + continue; + } + + if (change.OldLength >= TextModelOptions.DiffSizeThreshold || + change.NewLength >= TextModelOptions.DiffSizeThreshold) + { + change.IsOpaque = true; + result.Add(change); + continue; + // too big to even attempt a diff. This is aimed at the reload-a-giant-file scenario + // where OOM during diff is a distinct possibility. + } + + // Make sure to turn off IgnoreTrimWhiteSpace, since that doesn't make sense in + // the context of a minimal edit + StringDifferenceOptions options = new StringDifferenceOptions(differenceOptions.Value); + options.IgnoreTrimWhiteSpace = false; + IHierarchicalDifferenceCollection diffs; + + if (before != null && after != null) + { + // Don't materialize the strings when we know the before and after snapshots. They might be really huge and cause OOM. + // We will take this path in the file reload case. + diffs = textDifferencingService.DiffSnapshotSpans(new SnapshotSpan(before, change.OldSpan), + new SnapshotSpan(after, change.NewSpan), options); + } + else + { + // We need to evaluate the old and new text for the differencing service + string oldText = change.OldText; + string newText = change.NewText; + + if (oldText == newText) + { + // This change simply evaporates. This case occurs frequently in Venus and it is much + // better to short circuit it here than to fire up the differencing engine. + continue; + } + diffs = textDifferencingService.DiffStrings(oldText, newText, options); + } + + // Keep track of deltas for the "new" position, for sanity check + int delta = 0; + + // Add all the changes from the difference collection + result.AddRange(GetChangesFromDifferenceCollection(ref delta, change, change._oldText, change._newText, diffs)); + + // Sanity check + // If delta != 0, then we've constructed asymmetrical insertions and + // deletions, which should be impossible + Debug.Assert(delta == change.Delta, "Minimal edit delta should be equal to replaced text change's delta."); + } + } + // If we aren't computing minimal changes, then copy over the non-null changes + else + { + foreach (TextChange change in work) + { + if (change != null) + result.Add(change); + } + } + + return result; + } + + private static IList<TextChange> GetChangesFromDifferenceCollection(ref int delta, + TextChange originalChange, + StringRebuilder oldText, + StringRebuilder newText, + IHierarchicalDifferenceCollection diffCollection, + int leftOffset = 0, + int rightOffset = 0) + { + List<TextChange> changes = new List<TextChange>(); + + for (int i = 0; i < diffCollection.Differences.Count; i++) + { + Difference currentDiff = diffCollection.Differences[i]; + + Span leftDiffSpan = Translate(diffCollection.LeftDecomposition.GetSpanInOriginal(currentDiff.Left), leftOffset); + Span rightDiffSpan = Translate(diffCollection.RightDecomposition.GetSpanInOriginal(currentDiff.Right), rightOffset); + + // TODO: Since this evaluates differences lazily, we should add something here to *not* compute the next + // level of differences if we think it would be too expensive. + IHierarchicalDifferenceCollection nextLevelDiffs = diffCollection.GetContainedDifferences(i); + + if (nextLevelDiffs != null) + { + changes.AddRange(GetChangesFromDifferenceCollection(ref delta, originalChange, oldText, newText, nextLevelDiffs, leftDiffSpan.Start, rightDiffSpan.Start)); + } + else + { + TextChange minimalChange = new TextChange(originalChange.OldPosition + leftDiffSpan.Start, + oldText.GetSubText(leftDiffSpan), + newText.GetSubText(rightDiffSpan), + ComputeBoundaryConditions(originalChange, oldText, leftDiffSpan)); + + minimalChange.NewPosition = originalChange.NewPosition + rightDiffSpan.Start; + if (minimalChange.OldLength > 0 && minimalChange.NewLength > 0) + { + minimalChange.IsOpaque = true; + } + + delta += minimalChange.Delta; + changes.Add(minimalChange); + } + } + + return changes; + } + + + private static LineBreakBoundaryConditions ComputeBoundaryConditions(TextChange outerChange, StringRebuilder oldText, Span leftSpan) + { + LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None; + if (leftSpan.Start == 0) + { + bc = (outerChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.PrecedingReturn); + } + else if (oldText[leftSpan.Start - 1] == '\r') + { + bc = LineBreakBoundaryConditions.PrecedingReturn; + } + if (leftSpan.End == oldText.Length) + { + bc |= (outerChange.LineBreakBoundaryConditions & LineBreakBoundaryConditions.SucceedingNewline); + } + else if (oldText[leftSpan.End] == '\n') + { + bc |= LineBreakBoundaryConditions.SucceedingNewline; + } + return bc; + } + + private static Span Translate(Span span, int amount) + { + return new Span(span.Start + amount, span.Length); + } + + private static bool PriorTo(ITextChange denormalizedChange, ITextChange normalizedChange, int accumulatedDelta, int accumulatedNormalizedDelta) + { + // notice that denormalizedChange.OldPosition == denormalizedChange.NewPosition + if ((denormalizedChange.OldLength != 0) && (normalizedChange.OldLength != 0)) + { + // both deletions + return denormalizedChange.OldPosition <= normalizedChange.NewPosition - accumulatedDelta - accumulatedNormalizedDelta; + } + else + { + return denormalizedChange.OldPosition < normalizedChange.NewPosition - accumulatedDelta - accumulatedNormalizedDelta; + } + } + + /// <summary> + /// Given a set of changes against a particular snapshot, merge in a list of normalized changes that occurred + /// immediately after those changes (as part of the next snapshot) so that the merged changes refer to the + /// earlier snapshot. + /// </summary> + /// <param name="normalizedChanges">The list of changes to be merged.</param> + /// <param name="denormChangesWithSentinel">The list of changes into which to merge.</param> + public static void Denormalize(INormalizedTextChangeCollection normalizedChanges, List<TextChange> denormChangesWithSentinel) + { + // denormalizedChangesWithSentinel contains a list of changes that have been denormalized to the origin snapshot + // (the New positions in those changes are the same as the Old positions), and also has a sentinel at the end + // that has int.MaxValue for its position. + // args.Changes contains a list of changes that are normalized with respect to the most recent snapshot, so we know + // that they are independent and properly ordered -- thus we can perform a single merge pass against the + // denormalized changes. + + int rover = 0; + int accumulatedDelta = 0; + int accumulatedNormalizedDelta = 0; + List<ITextChange> normChanges = new List<ITextChange>(normalizedChanges); + for (int n = 0; n < normChanges.Count; ++n) + { + ITextChange normChange = normChanges[n]; + + // 1. skip past all denormalized changes that begin prior to the beginning of the current change. + + while (PriorTo(denormChangesWithSentinel[rover], normChange, accumulatedDelta, accumulatedNormalizedDelta)) + { + accumulatedDelta += denormChangesWithSentinel[rover++].Delta; + } + + // 2. normChange will be inserted at [rover], but it may need to be split + + if ((normChange.OldEnd - accumulatedDelta) > denormChangesWithSentinel[rover].OldPosition) + { + // split required. for example, text at 5..10 was deleted in snapshot 1, and then text at 0..10 was deleted + // in snapshot 2; the latter turns into two deletions in terms of snapshot 1: 0..5 and 10..15. + int deletionSuffix = (normChange.OldEnd - accumulatedDelta) - denormChangesWithSentinel[rover].OldPosition; + int deletionPrefix = normChange.OldLength - deletionSuffix; + int normDelta = normChange.NewPosition - normChange.OldPosition; + denormChangesWithSentinel.Insert + (rover, new TextChange(normChange.OldPosition - accumulatedDelta, + TextChange.ChangeOldSubText(normChange, 0, deletionPrefix), + TextChange.NewStringRebuilder(normChange), + LineBreakBoundaryConditions.None)); + accumulatedNormalizedDelta += normDelta; + + // the second part remains 'normalized' in case it needs to be split again + TextChange splitee = new TextChange(normChange.OldPosition + deletionPrefix, + TextChange.ChangeOldSubText(normChange, deletionPrefix, deletionSuffix), + StringRebuilder.Empty, + LineBreakBoundaryConditions.None); + splitee.NewPosition += normDelta; + normChanges.Insert(n + 1, splitee); + } + else + { + denormChangesWithSentinel.Insert + (rover, new TextChange(normChange.OldPosition - accumulatedDelta, + TextChange.OldStringRebuilder(normChange), + TextChange.NewStringRebuilder(normChange), + LineBreakBoundaryConditions.None)); + accumulatedNormalizedDelta += normChange.Delta; + } + + rover++; + } + } + + int ICollection<ITextChange>.Count => _changes.Count; + + bool ICollection<ITextChange>.IsReadOnly => true; + + ITextChange IList<ITextChange>.this[int index] + { + get => _changes[index]; + set => throw new System.NotSupportedException(); + } + + + int IList<ITextChange>.IndexOf(ITextChange item) + { + for (int i = 0; (i < _changes.Count); ++i) + { + if (item.Equals(_changes[i])) + return i; + } + + return -1; + } + + void IList<ITextChange>.Insert(int index, ITextChange item) + { + throw new System.NotSupportedException(); + } + + void IList<ITextChange>.RemoveAt(int index) + { + throw new System.NotSupportedException(); + } + + void ICollection<ITextChange>.Add(ITextChange item) + { + throw new System.NotSupportedException(); + } + + void ICollection<ITextChange>.Clear() + { + throw new System.NotSupportedException(); + } + + bool ICollection<ITextChange>.Contains(ITextChange item) + { + return ((IList<ITextChange>)this).IndexOf(item) != -1; + } + + void ICollection<ITextChange>.CopyTo(ITextChange[] array, int arrayIndex) + { + for (int i = 0; (i < _changes.Count); ++i) + { + array[i + arrayIndex] = _changes[i]; + } + } + + bool ICollection<ITextChange>.Remove(ITextChange item) + { + throw new System.NotSupportedException(); + } + + IEnumerator<ITextChange> IEnumerable<ITextChange>.GetEnumerator() + { + return _changes.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _changes.GetEnumerator(); + } + } +} diff --git a/src/Text/Impl/TextModel/PersistentSpan.cs b/src/Text/Impl/TextModel/PersistentSpan.cs new file mode 100644 index 0000000..79f205d --- /dev/null +++ b/src/Text/Impl/TextModel/PersistentSpan.cs @@ -0,0 +1,216 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Utilities; + using System; + using System.Diagnostics; + + internal sealed class PersistentSpan : IPersistentSpan + { + #region members + private PersistentSpanFactory _factory; + + private ITrackingSpan _span; //null for spans on closed documents or disposed spans + private ITextDocument _document; //null for spans on closed documents or disposed spans + private string _filePath; //null for spans on opened documents or disposed spans + private int _startLine; //these parameters are valid whether or not the document is open (but _start*,_end* may be stale). + private int _startIndex; + private int _endLine; + private int _endIndex; + private Span _nonTrackingSpan; + private bool _useLineIndex; + + private readonly SpanTrackingMode _trackingMode; + #endregion + + internal PersistentSpan(ITextDocument document, SnapshotSpan span, SpanTrackingMode trackingMode, PersistentSpanFactory factory) + { + //Arguments verified in factory + _document = document; + + _span = span.Snapshot.CreateTrackingSpan(span, trackingMode); + _trackingMode = trackingMode; + + _factory = factory; + } + + internal PersistentSpan(string filePath, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode, PersistentSpanFactory factory) + { + //Arguments verified in factory + _filePath = filePath; + + _useLineIndex = true; + _startLine = startLine; + _startIndex = startIndex; + _endLine = endLine; + _endIndex = endIndex; + + _trackingMode = trackingMode; + + _factory = factory; + } + + internal PersistentSpan(string filePath, Span span, SpanTrackingMode trackingMode, PersistentSpanFactory factory) + { + //Arguments verified in factory + _filePath = filePath; + + _useLineIndex = false; + _nonTrackingSpan = span; + + _trackingMode = trackingMode; + + _factory = factory; + } + + #region IPersistentSpan members + public bool IsDocumentOpen { get { return _document != null; } } + + public ITextDocument Document { get { return _document; } } + + public ITrackingSpan Span { get { return _span; } } + + public string FilePath + { + get + { + return (_document != null) ? _document.FilePath : _filePath; + } + } + + public bool TryGetStartLineIndex(out int startLine, out int startIndex) + { + if ((_document == null) && (_filePath == null)) + throw new ObjectDisposedException("PersistentSpan"); + + if (_span != null) + this.UpdateStartEnd(); + + startLine = _startLine; + startIndex = _startIndex; + + return ((_span != null) || _useLineIndex); + } + + public bool TryGetEndLineIndex(out int endLine, out int endIndex) + { + if ((_document == null) && (_filePath == null)) + throw new ObjectDisposedException("PersistentSpan"); + + if (_span != null) + this.UpdateStartEnd(); + + endLine = _endLine; + endIndex = _endIndex; + return ((_span != null) || _useLineIndex); + } + + public bool TryGetSpan(out Span span) + { + if ((_document == null) && (_filePath == null)) + throw new ObjectDisposedException("PersistentSpan"); + + if (_span != null) + this.UpdateStartEnd(); + + span = _nonTrackingSpan; + return ((_span != null) || !_useLineIndex); + } + #endregion + + #region IDisposable members + public void Dispose() + { + if ((_document != null) || (_filePath != null)) + { + _factory.Delete(this); + + _span = null; + _document = null; + _filePath = null; + } + } + #endregion + + #region private helpers + internal void DocumentClosed() + { + this.UpdateStartEnd(); + + //We set this to false when the document is closed because we have an accurate line/index and that is more stable + //than a simple offset. + _useLineIndex = true; + _nonTrackingSpan = new Span(0, 0); + + _filePath = _document.FilePath; + _document = null; + _span = null; + } + + internal void DocumentReopened(ITextDocument document) + { + _document = document; + + ITextSnapshot snapshot = document.TextBuffer.CurrentSnapshot; + + SnapshotPoint start; + SnapshotPoint end; + if (_useLineIndex) + { + start = PersistentSpan.LineIndexToSnapshotPoint(_startLine, _startIndex, snapshot); + end = PersistentSpan.LineIndexToSnapshotPoint(_endLine, _endIndex, snapshot); + + if (end < start) + { + //Guard against the case where _start & _end are something like (100,2) & (101, 1). + //Those points would pass the argument validation (since _endLine > _startLine) but + //would cause problems if the document has only 5 lines since they would map to + //(5, 2) & (5, 1). + end = start; + } + } + else + { + start = new SnapshotPoint(snapshot, Math.Min(_nonTrackingSpan.Start, snapshot.Length)); + end = new SnapshotPoint(snapshot, Math.Min(_nonTrackingSpan.End, snapshot.Length)); + } + + _span = snapshot.CreateTrackingSpan(new SnapshotSpan(start, end), _trackingMode); + + _filePath = null; + } + + private void UpdateStartEnd() + { + SnapshotSpan span = _span.GetSpan(_span.TextBuffer.CurrentSnapshot); + + _nonTrackingSpan = span; + + PersistentSpan.SnapshotPointToLineIndex(span.Start, out _startLine, out _startIndex); + PersistentSpan.SnapshotPointToLineIndex(span.End, out _endLine, out _endIndex); + } + + private static void SnapshotPointToLineIndex(SnapshotPoint p, out int line, out int index) + { + ITextSnapshotLine l = p.GetContainingLine(); + + line = l.LineNumber; + index = Math.Min(l.Length, p - l.Start); + } + + internal static SnapshotPoint LineIndexToSnapshotPoint(int line, int index, ITextSnapshot snapshot) + { + ITextSnapshotLine l = snapshot.GetLineFromLineNumber(Math.Min(line, snapshot.LineCount - 1)); + + return l.Start + Math.Min(index, l.Length); + } + #endregion + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/PersistentSpanFactory.cs b/src/Text/Impl/TextModel/PersistentSpanFactory.cs new file mode 100644 index 0000000..6ea3a3d --- /dev/null +++ b/src/Text/Impl/TextModel/PersistentSpanFactory.cs @@ -0,0 +1,295 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Utilities; + using System; + using System.Collections.Generic; + using System.ComponentModel.Composition; + using System.Diagnostics; + using System.IO; + + [Export(typeof(IPersistentSpanFactory))] + internal class PersistentSpanFactory : IPersistentSpanFactory + { + [Import] + internal ITextDocumentFactoryService TextDocumentFactoryService; + + private readonly Dictionary<object, FrugalList<PersistentSpan>> _spansOnDocuments = new Dictionary<object, FrugalList<PersistentSpan>>(); //Used for lock + + private bool _eventsHooked; + + #region IPersistentSpanFactory members + public bool CanCreate(ITextBuffer buffer) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + ITextDocument document; + return this.TextDocumentFactoryService.TryGetTextDocument(buffer, out document); + } + + public IPersistentSpan Create(SnapshotSpan span, SpanTrackingMode trackingMode) + { + ITextDocument document; + if (this.TextDocumentFactoryService.TryGetTextDocument(span.Snapshot.TextBuffer, out document)) + { + PersistentSpan persistentSpan = new PersistentSpan(document, span, trackingMode, this); + + this.AddSpan(document, persistentSpan); + + return persistentSpan; + } + + return null; + } + + public IPersistentSpan Create(ITextSnapshot snapshot, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode) + { + ITextDocument document; + if (this.TextDocumentFactoryService.TryGetTextDocument(snapshot.TextBuffer, out document)) + { + var start = PersistentSpan.LineIndexToSnapshotPoint(startLine, startIndex, snapshot); + var end = PersistentSpan.LineIndexToSnapshotPoint(endLine, endIndex, snapshot); + if (end < start) + { + end = start; + } + + PersistentSpan persistentSpan = new PersistentSpan(document, new SnapshotSpan(start, end), trackingMode, this); + + this.AddSpan(document, persistentSpan); + + return persistentSpan; + } + + return null; + } + + public IPersistentSpan Create(string filePath, int startLine, int startIndex, int endLine, int endIndex, SpanTrackingMode trackingMode) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentException("filePath"); + } + if (startLine < 0) + { + throw new ArgumentOutOfRangeException("startLine", "Must be non-negative."); + } + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException("startIndex", "Must be non-negative."); + } + if (endLine < startLine) + { + throw new ArgumentOutOfRangeException("endLine", "Must be >= startLine."); + } + if ((endIndex < 0) || ((startLine == endLine) && (endIndex < startIndex))) + { + throw new ArgumentOutOfRangeException("endIndex", "Must be non-negative and (endLine,endIndex) may not be before (startLine,startIndex)."); + } + if (((int)trackingMode < (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode > (int)(SpanTrackingMode.EdgeNegative))) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + + PersistentSpan persistentSpan = new PersistentSpan(filePath, startLine, startIndex, endLine, endIndex, trackingMode, this); + + this.AddSpan(new FileNameKey(filePath), persistentSpan); + + return persistentSpan; + } + + public IPersistentSpan Create(string filePath, Span span, SpanTrackingMode trackingMode) + { + if (string.IsNullOrEmpty(filePath)) + { + throw new ArgumentException("filePath"); + } + if (((int)trackingMode < (int)SpanTrackingMode.EdgeExclusive) || ((int)trackingMode > (int)(SpanTrackingMode.EdgeNegative))) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + + PersistentSpan persistentSpan = new PersistentSpan(filePath, span, trackingMode, this); + + this.AddSpan(new FileNameKey(filePath), persistentSpan); + + return persistentSpan; + } + #endregion + + internal bool IsEmpty { get { return _spansOnDocuments.Count == 0; } } //For unit tests + + private void AddSpan(object key, PersistentSpan persistentSpan) + { + lock (_spansOnDocuments) + { + FrugalList<PersistentSpan> spans; + if (!_spansOnDocuments.TryGetValue(key, out spans)) + { + this.EnsureEventsHooked(); + + spans = new FrugalList<PersistentSpan>(); + _spansOnDocuments.Add(key, spans); + } + + spans.Add(persistentSpan); + } + } + + private void EnsureEventsHooked() + { + if (!_eventsHooked) + { + _eventsHooked = true; + + this.TextDocumentFactoryService.TextDocumentCreated += OnTextDocumentCreated; + this.TextDocumentFactoryService.TextDocumentDisposed += OnTextDocumentDisposed; + } + } + + private void OnTextDocumentCreated(object sender, TextDocumentEventArgs e) + { + var path = new FileNameKey(e.TextDocument.FilePath); + FrugalList<PersistentSpan> spans; + lock (_spansOnDocuments) + { + if (_spansOnDocuments.TryGetValue(path, out spans)) + { + foreach (var span in spans) + { + span.DocumentReopened(e.TextDocument); + } + + _spansOnDocuments.Remove(path); + _spansOnDocuments.Add(e.TextDocument, spans); + } + } + } + + private void OnTextDocumentDisposed(object sender, TextDocumentEventArgs e) + { + FrugalList<PersistentSpan> spans; + lock (_spansOnDocuments) + { + if (_spansOnDocuments.TryGetValue(e.TextDocument, out spans)) + { + foreach (var span in spans) + { + span.DocumentClosed(); + } + + _spansOnDocuments.Remove(e.TextDocument); + + var path = new FileNameKey(e.TextDocument.FilePath); + FrugalList<PersistentSpan> existingSpansOnPath; + if (_spansOnDocuments.TryGetValue(path, out existingSpansOnPath)) + { + //Handle (badly) the case where a document is renamed to an existing closed document & then closed. + existingSpansOnPath.AddRange(spans); + } + else + { + _spansOnDocuments.Add(path, spans); + } + } + } + } + + internal void Delete(PersistentSpan span) + { + lock (_spansOnDocuments) + { + ITextDocument document = span.Document; + if (document != null) + { + FrugalList<PersistentSpan> spans; + if (_spansOnDocuments.TryGetValue(document, out spans)) + { + spans.Remove(span); + + if (spans.Count == 0) + { + //Last one ... remove all references to document. + _spansOnDocuments.Remove(document); + } + } + else + { + Debug.Fail("There should have been an entry in SpanOnDocuments."); + } + } + else + { + var path = new FileNameKey(span.FilePath); + FrugalList<PersistentSpan> spans; + if (_spansOnDocuments.TryGetValue(path, out spans)) + { + spans.Remove(span); + + if (spans.Count == 0) + { + //Last one ... remove all references to path. + _spansOnDocuments.Remove(path); + } + } + else + { + Debug.Fail("There should have been an entry in SpanOnDocuments."); + } + } + } + } + + private class FileNameKey + { + private readonly string _fileName; + private readonly int _hashCode; + + public FileNameKey(string fileName) + { + //Gracefully catch errors getting the full path (which can happen if the file name is on a protected share). + try + { + _fileName = Path.GetFullPath(fileName); + } + catch + { + //This shouldn't happen (we are generally passed names associated with documents that we are expecting to open so + //we should have access). If we fail, we will, at worst not get the same underlying document when people create + //persistent spans using unnormalized names. + _fileName = fileName; + } + + _hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(_fileName); + } + + //Override equality and hash code + public override int GetHashCode() + { + return _hashCode; + } + + public override bool Equals(object obj) + { + var other = obj as FileNameKey; + return (other != null) && string.Equals(_fileName, other._fileName, StringComparison.OrdinalIgnoreCase); + } + + public override string ToString() + { + return _fileName; + } + } + } +} diff --git a/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs b/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs new file mode 100644 index 0000000..5dbdfc3 --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/BaseProjectionBuffer.cs @@ -0,0 +1,228 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Utilities; + using System.Collections.ObjectModel; + + internal abstract class BaseProjectionBuffer : BaseBuffer, IProjectionBufferBase + { + #region State and construction + protected internal readonly IProjectionEditResolver resolver; + protected bool editApplicationInProgress; + protected List<TextContentChangedEventArgs> pendingContentChangedEventArgs = new List<TextContentChangedEventArgs>(); + + protected BaseProjectionBuffer(IProjectionEditResolver resolver, IContentType contentType, ITextDifferencingService textDifferencingService, GuardedOperations guardedOperations) + : base(contentType, 0, textDifferencingService, guardedOperations) + { + this.resolver = resolver; // null is OK + } + #endregion + + #region Source buffer event handling + internal abstract void OnSourceTextChanged(object sender, TextContentChangedEventArgs e); + + internal virtual void OnSourceBufferReadOnlyRegionsChanged(object sender, SnapshotSpanEventArgs e) + { + NormalizedSpanCollection mappedAffectedSpans = new NormalizedSpanCollection(this.CurrentBaseSnapshot.MapFromSourceSnapshot(e.Span)); + + if (mappedAffectedSpans.Count > 0) + { + ITextEventRaiser raiser = + new ReadOnlyRegionsChangedEventRaiser(new SnapshotSpan(this.currentSnapshot, + Span.FromBounds(mappedAffectedSpans[0].Start, + mappedAffectedSpans[mappedAffectedSpans.Count - 1].End))); + + this.group.BeginEdit(); + this.group.EnqueueEvents(raiser, this); + this.group.FinishEdit(); + } + } + + internal void OnSourceBufferContentTypeChanged(object sender, ContentTypeChangedEventArgs e) + { + TextContentChangedEventArgs args = new TextContentChangedEventArgs(e.Before, e.After, EditOptions.None, e.EditTag); + this.pendingContentChangedEventArgs.Add(args); + this.group.ScheduleIndependentEdit(this); + } + #endregion + + #region Editing Shortcuts + public new IProjectionSnapshot Insert(int position, string text) + { + return (IProjectionSnapshot)base.Insert(position, text); + } + + public new IProjectionSnapshot Delete(Span deleteSpan) + { + return (IProjectionSnapshot)base.Delete(deleteSpan); + } + + public new IProjectionSnapshot Replace(Span replaceSpan, string replaceWith) + { + return (IProjectionSnapshot)base.Replace(replaceSpan, replaceWith); + } + #endregion + + #region Read Only Region support + protected internal override bool IsReadOnlyImplementation(int position, bool isEdit) + { + if (this.CurrentBaseSnapshot.SpanCount == 0) + { + throw new InvalidOperationException(); + } + if (base.IsReadOnlyImplementation(position, isEdit)) + { + return true; + } + return AreBaseBuffersReadOnly(position, isEdit); + } + + private bool AreBaseBuffersReadOnly(int position, bool isEdit) + { + // a position on a seam between two or more source spans is readonly only + // if that position is readonly in all of the source buffers. + ReadOnlyCollection<SnapshotPoint> snapPoints = this.CurrentBaseSnapshot.MapInsertionPointToSourceSnapshots(position, null); + foreach (SnapshotPoint snapPoint in snapPoints) + { + BaseBuffer baseBuffer = (BaseBuffer)snapPoint.Snapshot.TextBuffer; + if (!baseBuffer.IsReadOnlyImplementation(snapPoint.Position, isEdit)) + { + return false; + } + } + return true; + } + + protected internal override bool IsReadOnlyImplementation(Span span, bool isEdit) + { + if (this.CurrentSnapshot.SpanCount == 0) + { + throw new InvalidOperationException(); + } + if (base.IsReadOnlyImplementation(span, isEdit)) + { + return true; + } + return AreBaseBuffersReadOnly(span, isEdit); + } + + public bool AreBaseBuffersReadOnly(Span span, bool isEdit) + { + if (span.Length == 0) + { + // treat like an insertion! + return AreBaseBuffersReadOnly(span.Start, isEdit); + } + else + { + IList<SnapshotSpan> snapSpans = this.CurrentBaseSnapshot.MapToSourceSnapshots(span); + foreach (SnapshotSpan snapSpan in snapSpans) + { + BaseBuffer baseBuffer = (BaseBuffer)snapSpan.Snapshot.TextBuffer; + if (baseBuffer.IsReadOnlyImplementation(snapSpan, isEdit)) + { + return true; + } + } + return false; + } + } + + protected internal override NormalizedSpanCollection GetReadOnlyExtentsImplementation(Span span) + { + // TODO: make something other than dead slow + + FrugalList<Span> result = new FrugalList<Span>(base.GetReadOnlyExtentsImplementation(span)); + + IList<SnapshotSpan> restrictionSpans = this.CurrentBaseSnapshot.MapToSourceSnapshotsForRead(span); + foreach (SnapshotSpan restrictionSpan in restrictionSpans) + { + SnapshotSpan? overlapSpan = (restrictionSpan.Span == span) ? restrictionSpan : restrictionSpan.Overlap(span); + if (overlapSpan.HasValue) + { + BaseBuffer baseBuffer = (BaseBuffer)restrictionSpan.Snapshot.TextBuffer; + NormalizedSpanCollection sourceExtents = baseBuffer.GetReadOnlyExtents(overlapSpan.Value); + foreach (Span sourceExtent in sourceExtents) + { + result.AddRange(this.CurrentBaseSnapshot.MapFromSourceSnapshot(new SnapshotSpan(restrictionSpan.Snapshot, sourceExtent))); + } + } + } + + return new NormalizedSpanCollection(result); + } + #endregion + + #region Abstract members + protected abstract BaseProjectionSnapshot CurrentBaseSnapshot { get; } + + public override abstract ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag); + + protected internal override abstract ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag); + + protected override abstract BaseSnapshot TakeSnapshot(); + + public new abstract IProjectionSnapshot CurrentSnapshot { get; } + + public abstract IList<ITextBuffer> SourceBuffers { get; } + + public abstract BaseBuffer.ITextEventRaiser PropagateSourceChanges(EditOptions options, object editTag); + #endregion + + #region Debug support + [Conditional("_DEBUG")] + protected void DumpPendingChanges(List<Tuple<ITextBuffer, List<TextChange>>> pendingSourceChanges) + { + if (BufferGroup.Tracing) + { + StringBuilder sb = new StringBuilder(string.Format(System.Globalization.CultureInfo.CurrentCulture, "Pending Changes in {0}\r\n", TextUtilities.GetTag(this))); + foreach (var p in pendingSourceChanges) + { + sb.AppendLine(TextUtilities.GetTag(p.Item1)); + for (int c = 0; c < p.Item2.Count - 1; ++c) // don't display sentinel + { + sb.AppendLine(p.Item2[c].ToString()); + } + } + sb.AppendLine(""); + Debug.Write(sb.ToString()); + } + } + + [Conditional("_DEBUG")] + protected void DumpPendingContentChangedEventArgs() + { + if (BufferGroup.Tracing) + { + StringBuilder sb = new StringBuilder(string.Format(System.Globalization.CultureInfo.CurrentCulture, "Pending Change Events in {0}\r\n", TextUtilities.GetTag(this))); + foreach (var args in this.pendingContentChangedEventArgs) + { + sb.Append(TextUtilities.GetTag(args.Before.TextBuffer)); + sb.Append(" V"); + sb.AppendLine(args.After.Version.VersionNumber.ToString()); + foreach (var change in args.Changes) + { + sb.AppendLine(change.ToString()); + } + } + sb.AppendLine(""); + Debug.Write(sb.ToString()); + } + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Projection/BaseProjectionSnapshot.cs b/src/Text/Impl/TextModel/Projection/BaseProjectionSnapshot.cs new file mode 100644 index 0000000..3f2a46a --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/BaseProjectionSnapshot.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Text; + using System.Threading; + + using Microsoft.VisualStudio.Text.Implementation; + + internal abstract class BaseProjectionSnapshot : BaseSnapshot, IProjectionSnapshot2 + { + #region State and Construction + protected int totalLength = 0; + protected int totalLineCount = 1; + + protected BaseProjectionSnapshot(ITextVersion2 version, StringRebuilder builder) + : base(version, builder) + { + } + #endregion + + public new abstract IProjectionBufferBase TextBuffer { get; } + + public ReadOnlyCollection<SnapshotPoint> MapToSourceSnapshots(int position) + { + return MapInsertionPointToSourceSnapshots(position, null); + } + + #region Abstract Members + /// <summary> + /// Given the position of a pure insertion (not a replacement), return the list of source points at which the inserted text + /// can be placed. This list has length greater than one only when the insertion point is on the seam of two or more source + /// spans. + /// </summary> + /// <param name="position">The position of the insertion into the projection buffer.</param> + /// <param name="excludedBuffer">Buffer to be ignored by virtue of its being a readonly literal buffer.</param> + internal abstract ReadOnlyCollection<SnapshotPoint> MapInsertionPointToSourceSnapshots(int position, ITextBuffer excludedBuffer); + + /// <summary> + /// Given the span of text to be deleted in a Replace operation, return the list of source spans to which it maps. Include any + /// zero-length spans either on the boundaries or in the middle of the replacement span; the idea is both map to the deleted + /// text and return the list of positions across which the inserted text can be placed. + /// </summary> + /// <param name="replacementSpan">The span of text to be replaced.</param> + /// <param name="excludedBuffer">Buffer to be ignored by virtue of its being a readonly literal buffer; only zero-length spans are possible + /// in this buffer.</param> + internal abstract ReadOnlyCollection<SnapshotSpan> MapReplacementSpanToSourceSnapshots(Span replacementSpan, ITextBuffer excludedBuffer); + + public abstract int SpanCount { get; } + public abstract ReadOnlyCollection<ITextSnapshot> SourceSnapshots { get; } + public abstract ITextSnapshot GetMatchingSnapshot(ITextBuffer textBuffer); + public abstract ITextSnapshot GetMatchingSnapshotInClosure(ITextBuffer targetBuffer); + public abstract ITextSnapshot GetMatchingSnapshotInClosure(Predicate<ITextBuffer> match); + public abstract ReadOnlyCollection<SnapshotSpan> GetSourceSpans(int startSpanIndex, int count); + public abstract ReadOnlyCollection<SnapshotSpan> GetSourceSpans(); + + public abstract SnapshotPoint MapToSourceSnapshot(int position); + public abstract SnapshotPoint MapToSourceSnapshot(int position, PositionAffinity affinity); + public abstract SnapshotPoint? MapFromSourceSnapshot(SnapshotPoint point, PositionAffinity affinity); + public abstract ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshots(Span span); + public abstract ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshotsForRead(Span span); + public abstract ReadOnlyCollection<Span> MapFromSourceSnapshot(SnapshotSpan span); + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Projection/BufferGraph.cs b/src/Text/Impl/TextModel/Projection/BufferGraph.cs new file mode 100644 index 0000000..87f42e8 --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/BufferGraph.cs @@ -0,0 +1,773 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + using Strings = Microsoft.VisualStudio.Text.Implementation.Strings; + + partial class BufferGraph : IBufferGraph + { + #region Private members + private readonly ITextBuffer topBuffer; + private readonly GuardedOperations guardedOperations; + internal Dictionary<ITextBuffer, FrugalList<IProjectionBufferBase>> importingProjectionBufferMap = new Dictionary<ITextBuffer, FrugalList<IProjectionBufferBase>>(); + internal List<WeakEventHookForBufferGraph> eventHooks = new List<WeakEventHookForBufferGraph>(); + #endregion + + #region Construction + public BufferGraph(ITextBuffer topBuffer, GuardedOperations guardedOperations) + { + if (topBuffer == null) + { + throw new ArgumentNullException("topBuffer"); + } + if (guardedOperations == null) + { + throw new ArgumentNullException("guardedOperations"); + } + + this.topBuffer = topBuffer; + this.guardedOperations = guardedOperations; + + this.importingProjectionBufferMap.Add(topBuffer, null); + // The top buffer has no targets, but it is put here so membership in this map can be used uniformly + // to determine whether a buffer is in the buffer graph + + // Subscribe to content type changed events on the toplevel buffer + this.eventHooks.Add(new WeakEventHookForBufferGraph(this, topBuffer)); + + IProjectionBufferBase projectionBufferBase = topBuffer as IProjectionBufferBase; + if (projectionBufferBase != null) + { + IList<ITextBuffer> sourceBuffers = projectionBufferBase.SourceBuffers; + FrugalList<ITextBuffer> dontCare = new FrugalList<ITextBuffer>(); + foreach (ITextBuffer sourceBuffer in sourceBuffers) + { + AddSourceBuffer(projectionBufferBase, sourceBuffer, dontCare); + } + } + } + #endregion + + #region Buffers + public ITextBuffer TopBuffer + { + get { return this.topBuffer; } + } + + public Collection<ITextBuffer> GetTextBuffers(Predicate<ITextBuffer> match) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + FrugalList<ITextBuffer> buffers = new FrugalList<ITextBuffer>(); + foreach (ITextBuffer buffer in this.importingProjectionBufferMap.Keys) + { + if (match(buffer)) + { + buffers.Add(buffer); + } + } + return new Collection<ITextBuffer>(buffers); + } + #endregion + + #region Mapping Point/Span Factories + public IMappingPoint CreateMappingPoint(SnapshotPoint point, PointTrackingMode trackingMode) + { + return new MappingPoint(point, trackingMode, this); + } + + public IMappingSpan CreateMappingSpan(SnapshotSpan span, SpanTrackingMode trackingMode) + { + return new MappingSpan(span, trackingMode, this); + } + #endregion + + #region Point Mapping + public SnapshotPoint? MapDownToFirstMatch(SnapshotPoint position, PointTrackingMode trackingMode, Predicate<ITextSnapshot> match, PositionAffinity affinity) + { + if (position.Snapshot == null) + { + throw new ArgumentNullException("position"); + } + if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + if (match == null) + { + throw new ArgumentNullException("match"); + } + if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) + { + throw new ArgumentOutOfRangeException("affinity"); + } + if (!this.importingProjectionBufferMap.ContainsKey(position.Snapshot.TextBuffer)) + { + return null; + } + + ITextBuffer currentBuffer = position.Snapshot.TextBuffer; + ITextSnapshot currentSnapshot = currentBuffer.CurrentSnapshot; + int currentPosition = position.TranslateTo(currentSnapshot, trackingMode).Position; + while (!match(currentSnapshot)) + { + IProjectionBufferBase projBuffer = currentBuffer as IProjectionBufferBase; + if (projBuffer == null) + { + return null; + } + IProjectionSnapshot projSnap = projBuffer.CurrentSnapshot; + if (projSnap.SourceSnapshots.Count == 0) + { + return null; + } + SnapshotPoint currentPoint = projSnap.MapToSourceSnapshot(currentPosition, affinity); + currentPosition = currentPoint.Position; + currentSnapshot = currentPoint.Snapshot; + currentBuffer = currentSnapshot.TextBuffer; + } + return new SnapshotPoint(currentSnapshot, currentPosition); + } + + public SnapshotPoint? MapDownToInsertionPoint(SnapshotPoint position, PointTrackingMode trackingMode, Predicate<ITextSnapshot> match) + { + if (position.Snapshot == null) + { + throw new ArgumentNullException("position"); + } + if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + if (match == null) + { + throw new ArgumentNullException("match"); + } + + ITextBuffer currentBuffer = position.Snapshot.TextBuffer; + int currentPosition = position.TranslateTo(currentBuffer.CurrentSnapshot, trackingMode); + ITextSnapshot currentSnapshot = currentBuffer.CurrentSnapshot; + while (!match(currentSnapshot)) + { + IProjectionBufferBase projBuffer = currentBuffer as IProjectionBufferBase; + if (projBuffer == null) + { + return null; + } + IProjectionSnapshot projSnap = projBuffer.CurrentSnapshot; + if (projSnap.SourceSnapshots.Count == 0) + { + return null; + } + SnapshotPoint currentPoint = projSnap.MapToSourceSnapshot(currentPosition); + currentPosition = currentPoint.Position; + currentSnapshot = currentPoint.Snapshot; + currentBuffer = currentSnapshot.TextBuffer; + } + return new SnapshotPoint(currentSnapshot, currentPosition); + } + + public SnapshotPoint? MapDownToBuffer(SnapshotPoint position, PointTrackingMode trackingMode, ITextBuffer targetBuffer, PositionAffinity affinity) + { + if (position.Snapshot == null) + { + throw new ArgumentNullException("position"); + } + if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + if (targetBuffer == null) + { + throw new ArgumentNullException("targetBuffer"); + } + if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) + { + throw new ArgumentOutOfRangeException("affinity"); + } + + ITextBuffer currentBuffer = position.Snapshot.TextBuffer; + ITextSnapshot currentSnapshot = currentBuffer.CurrentSnapshot; + int currentPosition = position.TranslateTo(currentSnapshot, trackingMode).Position; + + while (currentBuffer != targetBuffer) + { + IProjectionBufferBase projBuffer = currentBuffer as IProjectionBufferBase; + if (projBuffer == null) + { + return null; + } + IProjectionSnapshot projSnap = projBuffer.CurrentSnapshot; + if (projSnap.SourceSnapshots.Count == 0) + { + return null; + } + SnapshotPoint currentPoint = projSnap.MapToSourceSnapshot(currentPosition, affinity); + currentPosition = currentPoint.Position; + currentSnapshot = currentPoint.Snapshot; + currentBuffer = currentSnapshot.TextBuffer; + } + + return new SnapshotPoint(currentSnapshot, currentPosition); + } + + public SnapshotPoint? MapDownToSnapshot(SnapshotPoint position, PointTrackingMode trackingMode, ITextSnapshot targetSnapshot, PositionAffinity affinity) + { + if (targetSnapshot == null) + { + throw new ArgumentNullException("targetSnapshot"); + } + + SnapshotPoint? result = MapDownToBuffer(position, trackingMode, targetSnapshot.TextBuffer, affinity); + if (result.HasValue && (result.Value.Snapshot != targetSnapshot)) + { + result = result.Value.TranslateTo(targetSnapshot, trackingMode); + } + + return result; + } + + public SnapshotPoint? MapUpToBuffer(SnapshotPoint point, PointTrackingMode trackingMode, PositionAffinity affinity, ITextBuffer targetBuffer) + { + return CheckedMapUpToBuffer(point, trackingMode, snapshot => (snapshot.TextBuffer == targetBuffer), affinity); + } + + + public SnapshotPoint? MapUpToSnapshot(SnapshotPoint position, PointTrackingMode trackingMode, PositionAffinity affinity, ITextSnapshot targetSnapshot) + { + if (targetSnapshot == null) + { + throw new ArgumentNullException("targetSnapshot"); + } + + SnapshotPoint? result = MapUpToBuffer(position, trackingMode, affinity, targetSnapshot.TextBuffer); + if (result.HasValue && (result.Value.Snapshot != targetSnapshot)) + { + result = result.Value.TranslateTo(targetSnapshot, trackingMode); + } + + return result; + } + + public SnapshotPoint? MapUpToFirstMatch(SnapshotPoint point, PointTrackingMode trackingMode, Predicate<ITextSnapshot> match, PositionAffinity affinity) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + return CheckedMapUpToBuffer(point, trackingMode, match, affinity); + } + + private SnapshotPoint? CheckedMapUpToBuffer(SnapshotPoint point, PointTrackingMode trackingMode, Predicate<ITextSnapshot> match, PositionAffinity affinity) + { + if (point.Snapshot == null) + { + throw new ArgumentNullException("point"); + } + if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) + { + throw new ArgumentOutOfRangeException("affinity"); + } + + if (!this.importingProjectionBufferMap.ContainsKey(point.Snapshot.TextBuffer)) + { + return null; + } + else + { + SnapshotPoint currentPoint = point.TranslateTo(point.Snapshot.TextBuffer.CurrentSnapshot, trackingMode); + return MapUpToBufferGuts(currentPoint, affinity, match); + } + } + + private SnapshotPoint? MapUpToBufferGuts(SnapshotPoint currentPoint, PositionAffinity affinity, Predicate<ITextSnapshot> match) + { + if (match(currentPoint.Snapshot)) + { + return currentPoint; + } + FrugalList<IProjectionBufferBase> targetBuffers = this.importingProjectionBufferMap[currentPoint.Snapshot.TextBuffer]; + if (targetBuffers != null) // targetBuffers will be null if we fell off the top + { + foreach (IProjectionBufferBase projBuffer in targetBuffers) + { + SnapshotPoint? position = projBuffer.CurrentSnapshot.MapFromSourceSnapshot(currentPoint, affinity); + if (position.HasValue) + { + position = MapUpToBufferGuts(position.Value, affinity, match); + if (position.HasValue) + { + return position; + } + } + } + } + return null; + } + #endregion + + #region Span Mapping + public NormalizedSnapshotSpanCollection MapDownToFirstMatch(SnapshotSpan span, SpanTrackingMode trackingMode, Predicate<ITextSnapshot> match) + { + if (span.Snapshot == null) + { + throw new ArgumentNullException("span"); + } + if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + if (match == null) + { + throw new ArgumentNullException("match"); + } + + if (!this.importingProjectionBufferMap.ContainsKey(span.Snapshot.TextBuffer)) + { + return NormalizedSnapshotSpanCollection.Empty; + } + + ITextBuffer currentBuffer = span.Snapshot.TextBuffer; + SnapshotSpan currentTopSpan = span.TranslateTo(currentBuffer.CurrentSnapshot, trackingMode); + + if (match(currentBuffer.CurrentSnapshot)) + { + return new NormalizedSnapshotSpanCollection(currentTopSpan); + } + else if (!(currentBuffer is IProjectionBufferBase)) + { + return NormalizedSnapshotSpanCollection.Empty; + } + else + { + FrugalList<Span> targetSpans = new FrugalList<Span>(); + FrugalList<SnapshotSpan> spans = new FrugalList<SnapshotSpan>() { currentTopSpan }; + ITextSnapshot chosenSnapshot = null; + do + { + spans = MapDownOneLevel(spans, match, ref chosenSnapshot, ref targetSpans); + } while (spans.Count > 0); + return chosenSnapshot == null ? NormalizedSnapshotSpanCollection.Empty : new NormalizedSnapshotSpanCollection(chosenSnapshot, targetSpans); + } + } + + public NormalizedSnapshotSpanCollection MapDownToBuffer(SnapshotSpan span, SpanTrackingMode trackingMode, ITextBuffer targetBuffer) + { + if (targetBuffer == null) + { + throw new ArgumentNullException("targetBuffer"); + } + + if (!this.importingProjectionBufferMap.ContainsKey(targetBuffer)) + { + return NormalizedSnapshotSpanCollection.Empty; + } + else + { + return MapDownToFirstMatch(span, trackingMode, snapshot => (snapshot.TextBuffer == targetBuffer)); + } + } + + public NormalizedSnapshotSpanCollection MapDownToSnapshot(SnapshotSpan span, SpanTrackingMode trackingMode, ITextSnapshot targetSnapshot) + { + if (targetSnapshot == null) + { + throw new ArgumentNullException("targetSnapshot"); + } + + NormalizedSnapshotSpanCollection results = MapDownToBuffer(span, trackingMode, targetSnapshot.TextBuffer); + if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot)) + { + FrugalList<SnapshotSpan> translatedSpans = new FrugalList<SnapshotSpan>(); + foreach (SnapshotSpan s in results) + { + translatedSpans.Add(s.TranslateTo(targetSnapshot, trackingMode)); + } + + results = new NormalizedSnapshotSpanCollection(translatedSpans); + } + + return results; + } + + public NormalizedSnapshotSpanCollection MapUpToSnapshot(SnapshotSpan span, SpanTrackingMode trackingMode, ITextSnapshot targetSnapshot) + { + if (targetSnapshot == null) + { + throw new ArgumentNullException("targetSnapshot"); + } + + NormalizedSnapshotSpanCollection results = MapUpToBuffer(span, trackingMode, targetSnapshot.TextBuffer); + if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot)) + { + FrugalList<SnapshotSpan> translatedSpans = new FrugalList<SnapshotSpan>(); + foreach (SnapshotSpan s in results) + { + translatedSpans.Add(s.TranslateTo(targetSnapshot, trackingMode)); + } + + results = new NormalizedSnapshotSpanCollection(translatedSpans); + } + + return results; + } + + private static FrugalList<SnapshotSpan> MapDownOneLevel(FrugalList<SnapshotSpan> inputSpans, Predicate<ITextSnapshot> match, ref ITextSnapshot chosenSnapshot, ref FrugalList<Span> targetSpans) + { + FrugalList<SnapshotSpan> downSpans = new FrugalList<SnapshotSpan>(); + foreach (SnapshotSpan inputSpan in inputSpans) + { + IProjectionBufferBase projBuffer = (IProjectionBufferBase)inputSpan.Snapshot.TextBuffer; + IProjectionSnapshot projSnap = projBuffer.CurrentSnapshot; + if (projSnap.SourceSnapshots.Count > 0) + { + IList<SnapshotSpan> mappedSpans = projSnap.MapToSourceSnapshots(inputSpan); + for (int s = 0; s < mappedSpans.Count; ++s) + { + SnapshotSpan mappedSpan = mappedSpans[s]; + ITextBuffer mappedBuffer = mappedSpan.Snapshot.TextBuffer; + if (mappedBuffer.CurrentSnapshot == chosenSnapshot) + { + targetSpans.Add(mappedSpan.Span); + } + else if (chosenSnapshot == null && match(mappedBuffer.CurrentSnapshot)) + { + chosenSnapshot = mappedBuffer.CurrentSnapshot; + targetSpans.Add(mappedSpan.Span); + } + else + { + IProjectionBufferBase mappedProjBuffer = mappedBuffer as IProjectionBufferBase; + if (mappedProjBuffer != null) + { + downSpans.Add(mappedSpan); + } + } + } + } + } + return downSpans; + } + + public NormalizedSnapshotSpanCollection MapUpToBuffer(SnapshotSpan span, SpanTrackingMode trackingMode, ITextBuffer targetBuffer) + { + if (!this.importingProjectionBufferMap.ContainsKey(targetBuffer)) + { + return NormalizedSnapshotSpanCollection.Empty; + } + else + { + return CheckedMapUpToBuffer(span, trackingMode, snapshot => (snapshot.TextBuffer == targetBuffer)); + } + } + + public NormalizedSnapshotSpanCollection MapUpToFirstMatch(SnapshotSpan span, SpanTrackingMode trackingMode, Predicate<ITextSnapshot> match) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + return CheckedMapUpToBuffer(span, trackingMode, match); + } + + public NormalizedSnapshotSpanCollection CheckedMapUpToBuffer(SnapshotSpan span, SpanTrackingMode trackingMode, Predicate<ITextSnapshot> match) + { + if (span.Snapshot == null) + { + throw new ArgumentNullException("span"); + } + if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.EdgeNegative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + ITextBuffer buffer = span.Snapshot.TextBuffer; + if (!this.importingProjectionBufferMap.ContainsKey(buffer)) + { + return NormalizedSnapshotSpanCollection.Empty; + } + SnapshotSpan currentSpan = span.TranslateTo(buffer.CurrentSnapshot, trackingMode); + if (match(buffer.CurrentSnapshot)) + { + return new NormalizedSnapshotSpanCollection(currentSpan); + } + + ITextSnapshot chosenSnapshot = null; + FrugalList<Span> result = new FrugalList<Span>(); + FrugalList<SnapshotSpan> spans = new FrugalList<SnapshotSpan>() { currentSpan }; + do + { + spans = MapUpOneLevel(spans, ref chosenSnapshot, match, result); + } while (spans.Count > 0); + + if (chosenSnapshot == null) + { + return NormalizedSnapshotSpanCollection.Empty; + } + else + { + return new NormalizedSnapshotSpanCollection(chosenSnapshot, result); + } + } + + private FrugalList<SnapshotSpan> MapUpOneLevel(FrugalList<SnapshotSpan> spans, ref ITextSnapshot chosenSnapshot, Predicate<ITextSnapshot> match, FrugalList<Span> topSpans) + { + FrugalList<SnapshotSpan> upSpans = new FrugalList<SnapshotSpan>(); + foreach (SnapshotSpan span in spans) + { + FrugalList<IProjectionBufferBase> targetBuffers; + if (this.importingProjectionBufferMap.TryGetValue(span.Snapshot.TextBuffer, out targetBuffers)) + { + if (targetBuffers != null) // make sure we don't fall off the top + { + foreach (IProjectionBufferBase projBuffer in targetBuffers) + { + IList<Span> projSpans = projBuffer.CurrentSnapshot.MapFromSourceSnapshot(span); + if (projBuffer.CurrentSnapshot == chosenSnapshot) + { + topSpans.AddRange(projSpans); + } + else if (chosenSnapshot == null && match(projBuffer.CurrentSnapshot)) + { + chosenSnapshot = projBuffer.CurrentSnapshot; + topSpans.AddRange(projSpans); + } + else + { + foreach (Span projSpan in projSpans) + { + upSpans.Add(new SnapshotSpan(projBuffer.CurrentSnapshot, projSpan)); + } + } + } + } + } + } + return upSpans; + } + #endregion + + #region Event Handling + private class GraphEventRaiser : BaseBuffer.ITextEventRaiser + { + private BufferGraph graph; + private GraphBuffersChangedEventArgs args; + + public GraphEventRaiser(BufferGraph graph, GraphBuffersChangedEventArgs args) + { + this.graph = graph; + this.args = args; + } + + public void RaiseEvent(BaseBuffer baseBuffer, bool immediate) + { + this.graph.RaiseGraphBuffersChanged(this.args); + } + + public bool HasPostEvent + { + get { return false; } + } + } + + public void RaiseGraphBuffersChanged(GraphBuffersChangedEventArgs args) + { + var listeners = GraphBuffersChanged; + if (listeners != null) + { + this.guardedOperations.RaiseEvent(this, listeners, args); + } + } + + private void SourceBuffersChanged(object sender, ProjectionSourceBuffersChangedEventArgs e) + { + IProjectionBufferBase projBuffer = (IProjectionBufferBase)sender; + FrugalList<ITextBuffer> addedToGraphBuffers = new FrugalList<ITextBuffer>(); + FrugalList<ITextBuffer> removedFromGraphBuffers = new FrugalList<ITextBuffer>(); + + foreach (ITextBuffer addedBuffer in e.AddedBuffers) + { + AddSourceBuffer(projBuffer, addedBuffer, addedToGraphBuffers); + } + + foreach (ITextBuffer removedBuffer in e.RemovedBuffers) + { + RemoveSourceBuffer(projBuffer, removedBuffer, removedFromGraphBuffers); + } + + if (addedToGraphBuffers.Count > 0 || removedFromGraphBuffers.Count > 0) + { + var listeners = GraphBuffersChanged; + if (listeners != null) + { + ((BaseBuffer)projBuffer).group.EnqueueEvents + (new GraphEventRaiser(this, new GraphBuffersChangedEventArgs(addedToGraphBuffers, removedFromGraphBuffers)), null); + } + } + } + + private void AddSourceBuffer(IProjectionBufferBase projBuffer, ITextBuffer sourceBuffer, FrugalList<ITextBuffer> addedToGraphBuffers) + { + bool firstEncounter = false; + FrugalList<IProjectionBufferBase> importingProjectionBuffers; + if (!this.importingProjectionBufferMap.TryGetValue(sourceBuffer, out importingProjectionBuffers)) + { + // sourceBuffer is new to this buffer graph + addedToGraphBuffers.Add(sourceBuffer); + firstEncounter = true; + + importingProjectionBuffers = new FrugalList<IProjectionBufferBase>(); + this.importingProjectionBufferMap.Add(sourceBuffer, importingProjectionBuffers); + + this.eventHooks.Add(new WeakEventHookForBufferGraph(this, sourceBuffer)); + } + importingProjectionBuffers.Add(projBuffer); + + if (firstEncounter) + { + IProjectionBufferBase addedProjBufferBase = sourceBuffer as IProjectionBufferBase; + if (addedProjBufferBase != null) + { + foreach (ITextBuffer furtherSourceBuffer in addedProjBufferBase.SourceBuffers) + { + AddSourceBuffer(addedProjBufferBase, furtherSourceBuffer, addedToGraphBuffers); + } + } + } + } + + private void RemoveSourceBuffer(IProjectionBufferBase projBuffer, ITextBuffer sourceBuffer, FrugalList<ITextBuffer> removedFromGraphBuffers) + { + IList<IProjectionBufferBase> importingProjectionBuffers = this.importingProjectionBufferMap[sourceBuffer]; + importingProjectionBuffers.Remove(projBuffer); + if (importingProjectionBuffers.Count == 0) + { + removedFromGraphBuffers.Add(sourceBuffer); + this.importingProjectionBufferMap.Remove(sourceBuffer); + + for (int i = 0; (i < this.eventHooks.Count); ++i) + { + if (this.eventHooks[i].SourceBuffer == sourceBuffer) + { + this.eventHooks[i].UnsubscribeFromSourceBuffer(); + this.eventHooks.RemoveAt(i); + break; + } + } + + IProjectionBufferBase removedProjBufferBase = sourceBuffer as IProjectionBufferBase; + if (removedProjBufferBase != null) + { + foreach (ITextBuffer furtherSourceBuffer in removedProjBufferBase.SourceBuffers) + { + RemoveSourceBuffer(removedProjBufferBase, furtherSourceBuffer, removedFromGraphBuffers); + } + } + } + } + + protected void ContentTypeChanged(object sender, ContentTypeChangedEventArgs args) + { + // we do not subscribe to the immediate form of the sender's content type changed + // event, so we do not need to queue this event + var handler = GraphBufferContentTypeChanged; + if (handler != null) + { + handler(this, new GraphBufferContentTypeChangedEventArgs((ITextBuffer)sender, args.BeforeContentType, args.AfterContentType)); + } + } + + public event EventHandler<GraphBuffersChangedEventArgs> GraphBuffersChanged; + public event EventHandler<GraphBufferContentTypeChangedEventArgs> GraphBufferContentTypeChanged; + + #endregion + + /// <summary> + /// This an equivalent of the WeakEventHook, but for the buffer graph instead of being for a projection buffer. + /// </summary> + internal class WeakEventHookForBufferGraph + { + private readonly WeakReference<BufferGraph> _targetGraph; + private ITextBuffer _sourceBuffer; + + public WeakEventHookForBufferGraph(BufferGraph targetGraph, ITextBuffer sourceBuffer) + { + _targetGraph = new WeakReference<BufferGraph>(targetGraph); + _sourceBuffer = sourceBuffer; + + sourceBuffer.ContentTypeChanged += OnSourceBufferContentTypeChanged; + ProjectionBuffer projectionBuffer = sourceBuffer as ProjectionBuffer; + if (projectionBuffer != null) + { + projectionBuffer.SourceBuffersChangedImmediate += OnSourceBuffersChanged; + } + } + + public ITextBuffer SourceBuffer { get { return _sourceBuffer; } } + + public BufferGraph GetTargetGraph() // Not a property since it has side-effects + { + BufferGraph targetGraph; + if (_targetGraph.TryGetTarget(out targetGraph)) + { + return targetGraph; + } + + // The target buffer that was listening to events on the source buffer has died (no one was using it). + // Dead buffers tell no tales so they get to stop listening to tales as well. Unsubscribe from the + // events it hooked on the source buffer. + this.UnsubscribeFromSourceBuffer(); + return null; + } + + private void OnSourceBufferContentTypeChanged(object sender, ContentTypeChangedEventArgs e) + { + BufferGraph targetGraph = this.GetTargetGraph(); + if (targetGraph != null) + { + targetGraph.ContentTypeChanged(sender, e); + } + } + + private void OnSourceBuffersChanged(object sender, ProjectionSourceBuffersChangedEventArgs e) + { + BufferGraph targetGraph = this.GetTargetGraph(); + if (targetGraph != null) + { + targetGraph.SourceBuffersChanged(sender, e); + } + } + + public void UnsubscribeFromSourceBuffer() + { + if (_sourceBuffer != null) + { + _sourceBuffer.ContentTypeChanged -= OnSourceBufferContentTypeChanged; + ProjectionBuffer projectionBuffer = _sourceBuffer as ProjectionBuffer; + if (projectionBuffer != null) + { + projectionBuffer.SourceBuffersChangedImmediate -= OnSourceBuffersChanged; + } + + _sourceBuffer = null; + } + } + } + } +} diff --git a/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs b/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs new file mode 100644 index 0000000..7c47c06 --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/BufferGraphFactoryService.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.ComponentModel.Composition; + using Microsoft.VisualStudio.Text.Utilities; + + [Export(typeof(IBufferGraphFactoryService))] + internal sealed class BufferGraphFactoryService : IBufferGraphFactoryService + { + [Import] + internal GuardedOperations GuardedOperations = null; + + public IBufferGraph CreateBufferGraph(ITextBuffer textBuffer) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + return textBuffer.Properties.GetOrCreateSingletonProperty<BufferGraph>(() => (new BufferGraph(textBuffer, GuardedOperations))); + } + } +} diff --git a/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs b/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs new file mode 100644 index 0000000..f47748e --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/ElisionBuffer.cs @@ -0,0 +1,489 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Utilities; + + internal sealed class ElisionBuffer : BaseProjectionBuffer, IElisionBuffer + { + #region ElisionEdit class + private class ElisionEdit : Edit, ISubordinateTextEdit + { + private ElisionBuffer elisionBuffer; + private bool subordinate; + + public ElisionEdit(ElisionBuffer elisionBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, object editTag) + : base(elisionBuffer, originSnapshot, options, reiteratedVersionNumber, editTag) + { + this.elisionBuffer = elisionBuffer; + this.subordinate = true; + } + + public ITextBuffer TextBuffer + { + get { return this.elisionBuffer; } + } + + // this is the master edit path -- initiated from outside + protected override ITextSnapshot PerformApply() + { + CheckActive(); + this.applied = true; + this.subordinate = false; + + ITextSnapshot result = this.baseBuffer.currentSnapshot; + + if (this.changes.Count > 0) + { + this.elisionBuffer.group.PerformMasterEdit(this.elisionBuffer, this, this.options, this.editTag); + + if (!this.Canceled) + { + result = this.baseBuffer.currentSnapshot; + } + } + else + { + // vacuous edit + this.baseBuffer.editInProgress = false; + } + + return result; + } + + public void PreApply() + { + if (this.changes.Count > 0) + { + this.elisionBuffer.ComputeSourceEdits(this.changes); + } + } + + public void FinalApply() + { + if (this.changes.Count > 0 || this.elisionBuffer.pendingContentChangedEventArgs.Count > 0) + { + this.elisionBuffer.group.CancelIndependentEdit(this.elisionBuffer); // just in case + TextContentChangedEventRaiser raiser = this.elisionBuffer.IncorporateChanges(); + this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer); + raiser.RaiseEvent(this.baseBuffer, true); + } + + this.elisionBuffer.editInProgress = false; + this.elisionBuffer.editApplicationInProgress = false; + if (this.subordinate) + { + this.elisionBuffer.group.FinishEdit(); + } + } + + public override void CancelApplication() + { + if (!this.Canceled) + { + base.CancelApplication(); + this.elisionBuffer.editApplicationInProgress = false; + this.elisionBuffer.pendingContentChangedEventArgs.Clear(); + } + } + } + #endregion + + #region Private members, Construction, and Disposal + private ElisionBufferOptions elisionOptions; + private ElisionMap content; + private ElisionSnapshot currentElisionSnapshot; + private readonly ITextBuffer sourceBuffer; + private ITextSnapshot sourceSnapshot; + private WeakEventHook eventHook; + + public ElisionBuffer(IProjectionEditResolver resolver, + IContentType contentType, + ITextBuffer sourceBuffer, + NormalizedSpanCollection exposedSpans, + ElisionBufferOptions options, + ITextDifferencingService textDifferencingService, + GuardedOperations guardedOperations) + : base(resolver, contentType, textDifferencingService, guardedOperations) + { + Debug.Assert(sourceBuffer != null); + this.sourceBuffer = sourceBuffer; + this.sourceSnapshot = sourceBuffer.CurrentSnapshot; + + BaseBuffer baseSourceBuffer = (BaseBuffer)sourceBuffer; + + this.eventHook = new WeakEventHook(this, baseSourceBuffer); + + this.group = baseSourceBuffer.group; + this.group.AddMember(this); + + this.content = new ElisionMap(this.sourceSnapshot, exposedSpans); + + StringRebuilder newBuilder = StringRebuilder.Empty; + for (int i = 0; (i < exposedSpans.Count); ++i) + newBuilder = newBuilder.Append(BufferFactoryService.StringRebuilderFromSnapshotAndSpan(this.sourceSnapshot, exposedSpans[i])); + this.builder = newBuilder; + + this.elisionOptions = options; + this.currentVersion.SetLength(content.Length); + this.currentElisionSnapshot = new ElisionSnapshot(this, this.sourceSnapshot, base.currentVersion, this.builder, this.content, (options & ElisionBufferOptions.FillInMappingMode) != 0); + this.currentSnapshot = this.currentElisionSnapshot; + } + #endregion + + #region Source Buffer + public override IList<ITextBuffer> SourceBuffers + { + get { return new FrugalList<ITextBuffer>() { this.sourceBuffer }; } + } + + public ITextBuffer SourceBuffer + { + get { return this.sourceBuffer; } + } + + public ElisionBufferOptions Options + { + get { return this.elisionOptions; } + } + + #endregion + + #region ElisionSourceSpansChangedEventRaiser Class + private class ElisionSourceSpansChangedEventRaiser : ITextEventRaiser + { + private readonly ElisionSourceSpansChangedEventArgs args; + + public ElisionSourceSpansChangedEventRaiser(ElisionSourceSpansChangedEventArgs args) + { + this.args = args; + } + + public void RaiseEvent(BaseBuffer baseBuffer, bool immediate) + { + ElisionBuffer elBuffer = (ElisionBuffer)baseBuffer; + EventHandler<ElisionSourceSpansChangedEventArgs> spanHandlers = elBuffer.SourceSpansChanged; + if (spanHandlers != null) + { + spanHandlers(this, args); + } + + // now raise the text content changed event + baseBuffer.RawRaiseEvent(args, immediate); + } + + public bool HasPostEvent + { + get { return false; } + } + } + #endregion + + #region Span Editing + private class SpanEdit : TextBufferBaseEdit + { + private readonly ElisionBuffer elBuffer; + + public SpanEdit(ElisionBuffer elBuffer): base(elBuffer) + { + this.elBuffer = elBuffer; + } + + public IProjectionSnapshot Apply(NormalizedSpanCollection spansToElide, NormalizedSpanCollection spansToExpand) + { + this.applied = true; + try + { + if (spansToElide == null) + { + spansToElide = NormalizedSpanCollection.Empty; + } + if (spansToExpand == null) + { + spansToExpand = NormalizedSpanCollection.Empty; + } + if (spansToElide.Count > 0 || spansToExpand.Count > 0) + { + if ((spansToElide.Count > 0) && (spansToElide[spansToElide.Count - 1].End > this.elBuffer.sourceSnapshot.Length)) + { + throw new ArgumentOutOfRangeException("spansToElide"); + } + if ((spansToExpand.Count > 0) && (spansToExpand[spansToExpand.Count - 1].End > this.elBuffer.sourceSnapshot.Length)) + { + throw new ArgumentOutOfRangeException("spansToExpand"); + } + ElisionSourceSpansChangedEventArgs args = this.elBuffer.ApplySpanChanges(spansToElide, spansToExpand); + if (args != null) + { + ElisionSourceSpansChangedEventRaiser raiser = new ElisionSourceSpansChangedEventRaiser(args); + this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer); + raiser.RaiseEvent(this.baseBuffer, true); + } + this.baseBuffer.editInProgress = false; + } + else + { + this.baseBuffer.editInProgress = false; + } + } + finally + { + this.baseBuffer.group.FinishEdit(); + } + return this.elBuffer.currentElisionSnapshot; + } + } + + public IProjectionSnapshot ElideSpans(NormalizedSpanCollection spansToElide) + { + if (spansToElide == null) + { + throw new ArgumentNullException("spansToElide"); + } + return ModifySpans(spansToElide, null); + } + + public IProjectionSnapshot ExpandSpans(NormalizedSpanCollection spansToExpand) + { + if (spansToExpand == null) + { + throw new ArgumentNullException("spansToExpand"); + } + return ModifySpans(null, spansToExpand); + } + + public IProjectionSnapshot ModifySpans(NormalizedSpanCollection spansToElide, NormalizedSpanCollection spansToExpand) + { + using (SpanEdit spedit = new SpanEdit(this)) + { + return spedit.Apply(spansToElide, spansToExpand); + } + } + #endregion + + public override ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) + { + return new ElisionEdit(this, this.currentElisionSnapshot, options, reiteratedVersionNumber, editTag); + } + + protected internal override ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) + { + return new ElisionEdit(this, this.currentElisionSnapshot, options, reiteratedVersionNumber, editTag); + } + + internal void ComputeSourceEdits(FrugalList<TextChange> changes) + { + ITextEdit xedit = this.group.GetEdit((BaseBuffer)this.sourceBuffer); + foreach (TextChange change in changes) + { + if (change.OldLength > 0) + { + IList<SnapshotSpan> sourceDeletionSpans = this.currentElisionSnapshot.MapToSourceSnapshots(new Span(change.OldPosition, change.OldLength)); + foreach (SnapshotSpan sourceDeletionSpan in sourceDeletionSpans) + { + xedit.Delete(sourceDeletionSpan); + } + } + if (change.NewLength > 0) + { + // change includes an insertion + ReadOnlyCollection<SnapshotPoint> sourceInsertionPoints = this.currentElisionSnapshot.MapInsertionPointToSourceSnapshots(change.OldPosition, null); + + if (sourceInsertionPoints.Count == 1) + { + // the insertion point is unambiguous + xedit.Insert(sourceInsertionPoints[0].Position, change.NewText); + } + else + { + // the insertion is at the boundary of source spans + int[] insertionSizes = new int[sourceInsertionPoints.Count]; + + if (this.resolver != null) + { + this.resolver.FillInInsertionSizes(new SnapshotPoint(this.currentElisionSnapshot, change.OldPosition), + sourceInsertionPoints, change.NewText, insertionSizes); + } + + // if resolver was not provided, we just use zeros for the insertion sizes, which will push the entire insertion + // into the last slot. + + int pos = 0; + for (int i = 0; i < insertionSizes.Length; ++i) + { + // contend with any old garbage that the client passed back. + int size = (i == insertionSizes.Length - 1) + ? change.NewLength - pos + : Math.Min(insertionSizes[i], change.NewLength - pos); + if (size > 0) + { + xedit.Insert(sourceInsertionPoints[i].Position, TextChange.ChangeNewSubstring(change, pos, size)); + pos += size; + if (pos == change.NewLength) + { + break; // inserted text is used up, whether we've visited all of the insertionSizes or not + } + } + } + } + } + } + this.editApplicationInProgress = true; + } + + public override BaseBuffer.ITextEventRaiser PropagateSourceChanges(EditOptions options, object editTag) + { + TextContentChangedEventRaiser raiser = IncorporateChanges(); + raiser.RaiseEvent(this, true); + return raiser; + } + + #region ChangeApplication + + private ElisionSourceSpansChangedEventArgs ApplySpanChanges(NormalizedSpanCollection spansToElide, NormalizedSpanCollection spansToExpand) + { + ElisionSnapshot beforeSnapshot = this.currentElisionSnapshot; + FrugalList<TextChange> textChanges; + ElisionMap newContent = this.content.EditSpans(this.sourceSnapshot, spansToElide, spansToExpand, out textChanges); + if (newContent != this.content) + { + this.content = newContent; + INormalizedTextChangeCollection normalizedChanges = NormalizedTextChangeCollection.Create(textChanges); + SetCurrentVersionAndSnapshot(normalizedChanges); + return new ElisionSourceSpansChangedEventArgs(beforeSnapshot, this.currentElisionSnapshot, + spansToElide, spansToExpand, null); + } + else + { + return null; + } + } + #endregion + + #region Snapshots + + public override IProjectionSnapshot CurrentSnapshot + { + get { return this.currentElisionSnapshot; } + } + + protected override BaseProjectionSnapshot CurrentBaseSnapshot + { + get { return this.currentElisionSnapshot; } + } + + IElisionSnapshot IElisionBuffer.CurrentSnapshot + { + get { return this.currentElisionSnapshot; } + } + + protected override StringRebuilder GetDoppelgangerBuilder() + { + // If there are no elisions, we can simply reference the source snapshot. + var sourceSnapshot = this.sourceSnapshot; + if ((this.currentVersion.Length == sourceSnapshot.Length) && (sourceSnapshot is BaseSnapshot)) + { + return BufferFactoryService.StringRebuilderFromSnapshotAndSpan(sourceSnapshot, new Span(0, sourceSnapshot.Length)); + } + + return null; + } + + protected override BaseSnapshot TakeSnapshot() + { + this.currentElisionSnapshot = + new ElisionSnapshot(this, this.sourceSnapshot, this.currentVersion, this.builder, this.content, + (this.elisionOptions & ElisionBufferOptions.FillInMappingMode) != 0); + return this.currentElisionSnapshot; + } + #endregion + + private TextContentChangedEventRaiser IncorporateChanges() + { + Debug.Assert(this.sourceSnapshot == this.pendingContentChangedEventArgs[0].Before); + FrugalList<TextChange> projectedChanges = new FrugalList<TextChange>(); + + var args0 = this.pendingContentChangedEventArgs[0]; + INormalizedTextChangeCollection sourceChanges; + + // Separate the easy and common case: + if (this.pendingContentChangedEventArgs.Count == 1) + { + sourceChanges = args0.Changes; + this.sourceSnapshot = args0.After; + } + else + { + // there is more than one snapshot of the source buffer to deal with. Since the changes may be + // interleaved by position, we need to get a normalized list in sequence. First we denormalize the + // changes so they are all relative to the same single starting snapshot, then we normalize them again into + // a single list. + + // This relies crucially on the fact that we know something about the multiple snapshots: they were + // induced by projection span adjustments, and the changes across them are independent. That is to say, + // it is not the case that text inserted in one snapshot is deleted in a later snapshot in the series. + + DumpPendingContentChangedEventArgs(); + List<TextChange> denormalizedChanges = new List<TextChange>() { new TextChange(int.MaxValue, StringRebuilder.Empty, StringRebuilder.Empty, LineBreakBoundaryConditions.None) }; + for (int a = 0; a < this.pendingContentChangedEventArgs.Count; ++a) + { + NormalizedTextChangeCollection.Denormalize(this.pendingContentChangedEventArgs[a].Changes, denormalizedChanges); + } + DumpPendingChanges(new List<Tuple<ITextBuffer, List<TextChange>>>() { new Tuple<ITextBuffer, List<TextChange>>(this.sourceBuffer, denormalizedChanges) } ); + FrugalList<TextChange> slicedChanges = new FrugalList<TextChange>(); + + // remove the sentinel + for (int d = 0; d < denormalizedChanges.Count - 1; ++d) + { + slicedChanges.Add(denormalizedChanges[d]); + } + sourceChanges = NormalizedTextChangeCollection.Create(slicedChanges); + this.sourceSnapshot = this.pendingContentChangedEventArgs[this.pendingContentChangedEventArgs.Count - 1].After; + } + + if (sourceChanges.Count > 0) + { + this.content = this.content.IncorporateChanges(sourceChanges, projectedChanges, args0.Before, this.sourceSnapshot, this.currentElisionSnapshot); + } + + this.pendingContentChangedEventArgs.Clear(); + ElisionSnapshot beforeSnapshot = this.currentElisionSnapshot; + SetCurrentVersionAndSnapshot(NormalizedTextChangeCollection.Create(projectedChanges)); + this.editApplicationInProgress = false; + return new TextContentChangedEventRaiser(beforeSnapshot, this.currentElisionSnapshot, args0.Options, args0.EditTag); + } + + #region Event Handling + internal override void OnSourceTextChanged(object sender, TextContentChangedEventArgs e) + { + this.pendingContentChangedEventArgs.Add(e); + + if (!this.editApplicationInProgress) + { + // We had better be a member of the same group as the buffer that we just heard from + Debug.Assert(this.group.MembersContains(e.After.TextBuffer)); + // Let the buffer group decide when to issue events (this allows us to coalesce multiple snapshots + // from the source buffer (this can happen if the source buffer is a projection buffer) into a single snapshot here. + this.group.ScheduleIndependentEdit(this); + } + } + #endregion + + #region Public Events + public event EventHandler<ElisionSourceSpansChangedEventArgs> SourceSpansChanged; + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Projection/ElisionMap.cs b/src/Text/Impl/TextModel/Projection/ElisionMap.cs new file mode 100644 index 0000000..465077f --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/ElisionMap.cs @@ -0,0 +1,330 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + + using Strings = Microsoft.VisualStudio.Text.Implementation.Strings; + + internal class ElisionMap + { + private readonly ElisionMapNode root; + private readonly int spanCount; + + #region Construction + /// <summary> + /// Construct an elision map given the size of the source buffer and the set of exposed spans. + /// </summary> + public ElisionMap(ITextSnapshot sourceSnapshot, NormalizedSpanCollection exposedSpans) + { + this.spanCount = exposedSpans.Count; + if (exposedSpans.Count == 0) + { + // everything is hidden: one leaf node + this.root = new ElisionMapNode(0, sourceSnapshot.Length, 0, sourceSnapshot.LineCount - 1, true); + } + else + { + // build a nicely balanced tree + // calculate all the line numbers we'll need in advance + int[] lineNumbers = new int[exposedSpans.Count * 2 + 1]; + for (int es = 0; es < exposedSpans.Count; ++es) + { + lineNumbers[es * 2] = sourceSnapshot.GetLineNumberFromPosition(exposedSpans[es].Start); + lineNumbers[es * 2 + 1] = sourceSnapshot.GetLineNumberFromPosition(exposedSpans[es].End); + } + lineNumbers[exposedSpans.Count * 2] = sourceSnapshot.LineCount - 1; + this.root = Build(new SnapshotSpan(sourceSnapshot, 0, sourceSnapshot.Length), exposedSpans, lineNumbers, new Span(0, exposedSpans.Count)); + } + if (BufferGroup.Tracing) + { + root.Dump(0); + } + } + + private ElisionMap(ElisionMapNode root, int spanCount) + { + this.root = root; + this.spanCount = spanCount; + } + + /// <summary> + /// Recursively build span tree. + /// </summary> + /// <param name="sourceSpan">SnapshotSpan over the source segment covered by this subtree, including both exposed and hidden text.</param> + /// <param name="exposedSpans">Set of exposed spans for the entire buffer.</param> + /// <param name="lineNumbers">Precomputed line numbers at all seams.</param> + /// <param name="slice">The slice of exposed spans in this subtree.</param> + /// <returns></returns> + private ElisionMapNode Build(SnapshotSpan sourceSpan, NormalizedSpanCollection exposedSpans, int[] lineNumbers, Span slice) + { + int mid = slice.Start + (slice.Length / 2); + Span midExposedSpan = exposedSpans[mid]; + + Span leftSlice = Span.FromBounds(slice.Start, mid); + ElisionMapNode left; + Span leftSpan; + if (leftSlice.Length > 0) + { + leftSpan = Span.FromBounds(sourceSpan.Start, midExposedSpan.Start); + left = Build(new SnapshotSpan(sourceSpan.Snapshot, leftSpan), exposedSpans, lineNumbers, leftSlice); + Debug.Assert(left.TotalSourceSize == leftSpan.Length); + } + else if (slice.Start == 0 && midExposedSpan.Start != 0) + { + Debug.Assert(sourceSpan.Start == 0); + leftSpan = Span.FromBounds(0, midExposedSpan.Start); + // the beginning of the buffer is elided. Do the special case of the first + // node in the tree having an exposed size of zero. + // TODO: figure this out in advance so we don't screw up the balance of the tree + left = new ElisionMapNode(0, leftSpan.Length, 0, + TextUtilities.ScanForLineCount(sourceSpan.Snapshot.GetText(leftSpan)), + true); + } + else + { + leftSpan = new Span(midExposedSpan.Start, 0); + left = null; + } + + Span rightSlice = Span.FromBounds(mid + 1, slice.End); + ElisionMapNode right; + Span rightSpan; + if (rightSlice.Length > 0) + { + rightSpan = Span.FromBounds(exposedSpans[mid + 1].Start, sourceSpan.End); + right = Build(new SnapshotSpan(sourceSpan.Snapshot, rightSpan), exposedSpans, lineNumbers, rightSlice); + Debug.Assert(right.TotalSourceSize == rightSpan.Length); + } + else + { + rightSpan = new Span(sourceSpan.End, 0); + right = null; + } + + Span midHiddenSpan = Span.FromBounds(midExposedSpan.End, rightSpan.Start); + ITextSnapshot sourceSnapshot = sourceSpan.Snapshot; + + int startLineNumber = lineNumbers[2 * mid]; + int endExposedLineNumber = lineNumbers[2 * mid + 1]; + int endSourceLineNumber = lineNumbers[2 * mid + 2]; + + int exposedLineBreakCount = endExposedLineNumber - startLineNumber; + int hiddenLineBreakCount = endSourceLineNumber - endExposedLineNumber; + + return new ElisionMapNode(midExposedSpan.Length, + sourceSpan.Length - (leftSpan.Length + rightSpan.Length), + exposedLineBreakCount, + exposedLineBreakCount + hiddenLineBreakCount, + left, + right, + false); + } + #endregion + + public int Length + { + get { return this.root.TotalExposedSize; } + } + + public int LineCount + { + get { return this.root.TotalExposedLineBreakCount + 1; } + } + + public int SpanCount + { + get { return spanCount; } + } + + public ElisionMap EditSpans(ITextSnapshot sourceSnapshot, + NormalizedSpanCollection spansToElide, + NormalizedSpanCollection spansToExpand, + out FrugalList<TextChange> textChanges) + { + textChanges = new FrugalList<TextChange>(); + NormalizedSpanCollection beforeSourceSpans = new NormalizedSnapshotSpanCollection(GetSourceSpans(sourceSnapshot, 0, this.spanCount)); + + NormalizedSpanCollection afterElisionSourceSpans = NormalizedSpanCollection.Difference(beforeSourceSpans, spansToElide); + NormalizedSpanCollection elisionChangeSpans = NormalizedSpanCollection.Difference(beforeSourceSpans, afterElisionSourceSpans); + foreach (Span s in elisionChangeSpans) + { + textChanges.Add(TextChange.Create(this.root.MapFromSourceSnapshotToNearest(s.Start, 0), + BufferFactoryService.StringRebuilderFromSnapshotAndSpan(sourceSnapshot, s), + StringRebuilder.Empty, + sourceSnapshot)); + } + + NormalizedSpanCollection afterExpansionSourceSpans = NormalizedSpanCollection.Union(afterElisionSourceSpans, spansToExpand); + NormalizedSpanCollection expansionChangeSpans = NormalizedSpanCollection.Difference(afterExpansionSourceSpans, afterElisionSourceSpans); + foreach (Span s in expansionChangeSpans) + { + textChanges.Add(TextChange.Create(this.root.MapFromSourceSnapshotToNearest(s.Start, 0), + StringRebuilder.Empty, + BufferFactoryService.StringRebuilderFromSnapshotAndSpan(sourceSnapshot, s), + sourceSnapshot)); + } + + return textChanges.Count > 0 ? new ElisionMap(sourceSnapshot, afterExpansionSourceSpans) : this; + } + + public IList<SnapshotSpan> GetSourceSpans(ITextSnapshot sourceSnapshot, int startSpanIndex, int count) + { + FrugalList<SnapshotSpan> result = new FrugalList<SnapshotSpan>(); + int rank = -1; + int sourcePrefixSize = 0; + this.root.GetSourceSpans(sourceSnapshot, ref rank, ref sourcePrefixSize, startSpanIndex, startSpanIndex + count, result); + return result; + } + + public SnapshotPoint MapToSourceSnapshot(ITextSnapshot sourceSnapshot, int position, PositionAffinity affinity) + { + return this.root.MapToSourceSnapshot(sourceSnapshot, position, 0, affinity); + } + + public SnapshotPoint? MapFromSourceSnapshot(ITextSnapshot snapshot, int position) + { + // affinity is superfluous for elision buffers + return this.root.MapFromSourceSnapshot(snapshot, position, 0); + } + + public SnapshotPoint MapFromSourceSnapshotToNearest(ITextSnapshot snapshot, int position) + { + return new SnapshotPoint(snapshot, this.root.MapFromSourceSnapshotToNearest(position, 0)); + } + + public void MapToSourceSnapshots(IElisionSnapshot elisionSnapshot, Span span, FrugalList<SnapshotSpan> result) + { + if (span.Length == 0) + { + span = MapNullSpansToSourceSnapshots(elisionSnapshot, span, result); + } + else + { + this.root.MapToSourceSnapshots(elisionSnapshot.SourceSnapshot, span, 0, result); + } + +#if DEBUG + int length = 0; + foreach (SnapshotSpan ss in result) + { + length += ss.Length; + } + Debug.Assert(length == span.Length); +#endif + } + + private Span MapNullSpansToSourceSnapshots(IElisionSnapshot elisionSnapshot, Span span, FrugalList<SnapshotSpan> result) + { + // TODO: this is identical to projection snapshot; can it be shared? + FrugalList<SnapshotPoint> points = MapInsertionPointToSourceSnapshots(elisionSnapshot, span.Start); + for (int p = 0; p < points.Count; ++p) + { + SnapshotPoint point = points[p]; + SnapshotSpan mappedSpan = new SnapshotSpan(point.Snapshot, point.Position, 0); + if (result.Count == 0 || mappedSpan != result[result.Count - 1]) + { + result.Add(mappedSpan); + } + } + return span; + } + + public void MapToSourceSnapshotsInFillInMode(ITextSnapshot sourceSnapshot, Span span, FrugalList<SnapshotSpan> result) + { + SnapshotPoint? start; + SnapshotPoint? end; + if (span.Length == 0) + { + start = span.Start == 0 ? new SnapshotPoint(sourceSnapshot, 0) : this.root.MapToSourceSnapshot(sourceSnapshot, span.Start, 0, PositionAffinity.Predecessor); + end = span.End == this.Length ? new SnapshotPoint(sourceSnapshot, sourceSnapshot.Length) : this.root.MapToSourceSnapshot(sourceSnapshot, span.End, 0, PositionAffinity.Successor); + } + else + { + start = this.root.MapToSourceSnapshot(sourceSnapshot, span.Start, 0, PositionAffinity.Successor); + end = this.root.MapToSourceSnapshot(sourceSnapshot, span.End, 0, PositionAffinity.Predecessor); + } + Debug.Assert(start.HasValue); + Debug.Assert(end.HasValue); + result.Add(new SnapshotSpan(sourceSnapshot, Span.FromBounds(start.Value, end.Value))); + } + + public void MapFromSourceSnapshot(Span span, FrugalList<Span> result) + { + if (span.Length == 0) + { + this.root.MapNullSpanFromSourceSnapshot(span, 0, result); + } + else + { + this.root.MapFromSourceSnapshot(span, 0, result); + } + } + + public FrugalList<SnapshotPoint> MapInsertionPointToSourceSnapshots(IElisionSnapshot elisionSnapshot, int exposedPosition) + { + FrugalList<SnapshotPoint> points = new FrugalList<SnapshotPoint>(); + this.root.MapInsertionPointToSourceSnapshots(elisionSnapshot, exposedPosition, 0, points); + return points; + } + + public int GetLineNumberFromPosition(int position, ITextSnapshot sourceSnapshot) + { + return this.root.GetLineNumberFromPosition(sourceSnapshot, position, 0, 0); + } + + public ElisionMap IncorporateChanges(INormalizedTextChangeCollection sourceChanges, + FrugalList<TextChange> projectedChanges, + ITextSnapshot beforeSourceSnapshot, + ITextSnapshot sourceSnapshot, + ITextSnapshot beforeElisionSnapshot) + { + ElisionMapNode newRoot = this.root; + int accumulatedProjectedDelta = 0; + foreach (ITextChange sourceChange in sourceChanges) + { + int accumulatedDelete = 0; + int incrementalAccumulatedProjectedDelta = 0; + StringRebuilder newText = TextChange.NewStringRebuilder(sourceChange); + + newRoot = newRoot.IncorporateChange(beforeSourceSnapshot : beforeSourceSnapshot, + afterSourceSnapshot : sourceSnapshot, + beforeElisionSnapshot : beforeElisionSnapshot, + sourceInsertionPosition : sourceChange.NewLength > 0 ? sourceChange.NewPosition : (int?)null, + newText : newText, + sourceDeletionSpan : new Span(sourceChange.NewPosition, sourceChange.OldLength), + absoluteSourceOldPosition : sourceChange.OldPosition, + absoluteSourceNewPosition : sourceChange.NewPosition, + projectedPrefixSize : 0, + projectedChanges : projectedChanges, + incomingAccumulatedDelta : accumulatedProjectedDelta, + outgoingAccumulatedDelta : ref incrementalAccumulatedProjectedDelta, + accumulatedDelete : ref accumulatedDelete); + accumulatedProjectedDelta += incrementalAccumulatedProjectedDelta; + } + if (newRoot.TotalSourceSize != sourceSnapshot.Length) + { + Debug.Fail(String.Format(System.Globalization.CultureInfo.InvariantCulture, + "Change incorporation length inconsistency. Elision:{0} Source:{1}", newRoot.TotalSourceSize, sourceSnapshot.Length)); + throw new InvalidOperationException(Strings.InvalidLengthCalculation); + } + if (newRoot.TotalSourceLineBreakCount + 1 != sourceSnapshot.LineCount) + { + Debug.Fail(String.Format(System.Globalization.CultureInfo.InvariantCulture, + "Change incorporation line count inconsistency. Elision:{0} Source:{1}", newRoot.TotalSourceLineBreakCount + 1, sourceSnapshot.LineCount)); + throw new InvalidOperationException(Strings.InvalidLineCountCalculation); + } + return new ElisionMap(newRoot, this.spanCount); + } + } +} diff --git a/src/Text/Impl/TextModel/Projection/ElisionMapNode.cs b/src/Text/Impl/TextModel/Projection/ElisionMapNode.cs new file mode 100644 index 0000000..8a93c6d --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/ElisionMapNode.cs @@ -0,0 +1,1184 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + + internal class ElisionMapNode + { + #region State + private readonly bool leftmostElision; // it would be better to avoid burning this space in every node + + private readonly int exposedSize; + private readonly int sourceSize; + private readonly int exposedLineBreakCount; + private readonly int sourceLineBreakCount; + + private readonly int totalExposedSize; + private readonly int totalSourceSize; + private readonly int totalExposedLineBreakCount; + private readonly int totalSourceLineBreakCount; + + private readonly ElisionMapNode left; + private readonly ElisionMapNode right; + #endregion + + #region Debugging Support + public override string ToString() + { + return String.Format(System.Globalization.CultureInfo.InvariantCulture, + "Exp:{0} Src:{1} TExp:{2} Tsrc:{3}", this.exposedSize, this.sourceSize, this.totalExposedSize, this.totalSourceSize); + } + #endregion + + #region Constructors + // leaf constructor + public ElisionMapNode(int exposedSize, int sourceSize, int exposedLineBreakCount, int sourceLineBreakCount, bool leftmostElision) + { + this.exposedSize = exposedSize; + this.sourceSize = sourceSize; + this.exposedLineBreakCount = exposedLineBreakCount; + this.sourceLineBreakCount = sourceLineBreakCount; + + this.totalExposedSize = exposedSize; + this.totalSourceSize = sourceSize; + this.totalExposedLineBreakCount = exposedLineBreakCount; + this.totalSourceLineBreakCount = sourceLineBreakCount; + + this.leftmostElision = leftmostElision; + } + + // internal node constructor + public ElisionMapNode(int exposedSize, int sourceSize, int exposedLineBreakCount, int sourceLineBreakCount, ElisionMapNode left, ElisionMapNode right, bool leftmostElision) + { + this.exposedSize = exposedSize; + this.sourceSize = sourceSize; + this.exposedLineBreakCount = exposedLineBreakCount; + this.sourceLineBreakCount = sourceLineBreakCount; + this.left = left; + this.right = right; + + this.totalExposedSize = LeftTotalExposedSize() + exposedSize + RightTotalExposedSize(); + this.totalSourceSize = LeftTotalSourceSize() + sourceSize + RightTotalSourceSize(); + this.totalExposedLineBreakCount = LeftTotalExposedLineBreakCount() + exposedLineBreakCount + RightTotalExposedLineBreakCount(); + this.totalSourceLineBreakCount = LeftTotalSourceLineBreakCount() + sourceLineBreakCount + RightTotalSourceLineBreakCount(); + + this.leftmostElision = leftmostElision; + } + #endregion + + #region Public Properties + public int TotalExposedSize + { + get { return this.totalExposedSize; } + } + + public int TotalSourceSize + { + get { return this.totalSourceSize; } + } + + public int TotalExposedLineBreakCount + { + get { return this.totalExposedLineBreakCount; } + } + + public int TotalSourceLineBreakCount + { + get { return this.totalSourceLineBreakCount; } + } + #endregion + + #region Helpers + private int LeftTotalSourceSize() + { + return this.left == null ? 0 : this.left.totalSourceSize; + } + + private int LeftTotalExposedSize() + { + return this.left == null ? 0 : this.left.totalExposedSize; + } + + private int LeftTotalHiddenSize() + { + return this.left == null ? 0 : this.left.totalSourceSize - this.left.totalExposedSize; + } + + private int LeftTotalExposedLineBreakCount() + { + return this.left == null ? 0 : this.left.totalExposedLineBreakCount; + } + + private int LeftTotalSourceLineBreakCount() + { + return this.left == null ? 0 : this.left.totalSourceLineBreakCount; + } + + private int LeftTotalHiddenLineBreakCount() + { + return this.left == null ? 0 : this.left.totalSourceLineBreakCount - this.left.totalExposedLineBreakCount; + } + + private int RightTotalSourceSize() + { + return this.right == null ? 0 : this.right.totalSourceSize; + } + + private int RightTotalExposedSize() + { + return this.right == null ? 0 : this.right.totalExposedSize; + } + + private int RightTotalExposedLineBreakCount() + { + return this.right == null ? 0 : this.right.totalExposedLineBreakCount; + } + + private int RightTotalSourceLineBreakCount() + { + return this.right == null ? 0 : this.right.totalSourceLineBreakCount; + } + + public void Dump(int level) + { + if (this.left != null) + { + this.left.Dump(level + 1); + } + Debug.Write(String.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}{1},{2}({3},{4})\n", new string(' ', level * 3), this.exposedSize, this.sourceSize, this.totalExposedSize, this.totalSourceSize)); + if (this.right != null) + { + this.right.Dump(level + 1); + } + } + #endregion + + #region Source Spans + public void GetSourceSpans(ITextSnapshot sourceSnapshot, ref int rank, ref int sourcePrefixSize, int startSpanIndex, int endSpanIndex, IList<SnapshotSpan> result) + { + // We don't store explicit rank information in the nodes, so we walk the tree in inorder until we + // get to the starting position and continue until we have what we need. + if (this.left != null) + { + this.left.GetSourceSpans(sourceSnapshot, ref rank, ref sourcePrefixSize, startSpanIndex, endSpanIndex, result); + } + + if (!this.leftmostElision) + { + // don't count the possible special case node at the beginning; + rank++; + } + if (rank >= startSpanIndex && rank < endSpanIndex) + { + result.Add(new SnapshotSpan(sourceSnapshot, sourcePrefixSize, this.exposedSize)); + } + sourcePrefixSize += this.sourceSize; + + if (this.right != null) + { + this.right.GetSourceSpans(sourceSnapshot, ref rank, ref sourcePrefixSize, startSpanIndex, endSpanIndex, result); + } + } + #endregion + + #region Mapping + public SnapshotPoint MapToSourceSnapshot(ITextSnapshot sourceSnapshot, int exposedPosition, int sourcePrefixLength, PositionAffinity affinity) + { + Debug.Assert(exposedPosition >= 0); + Debug.Assert(exposedPosition <= this.totalExposedSize); + + int leftTotalExposedSize = LeftTotalExposedSize(); + + if (((affinity == PositionAffinity.Predecessor) && (exposedPosition <= leftTotalExposedSize) && left != null && !left.leftmostElision) || + ((affinity == PositionAffinity.Successor) && (exposedPosition < leftTotalExposedSize))) + { + return this.left.MapToSourceSnapshot(sourceSnapshot : sourceSnapshot, + exposedPosition : exposedPosition, + sourcePrefixLength : sourcePrefixLength, + affinity : affinity); + } + + else if (((affinity == PositionAffinity.Predecessor) && (exposedPosition <= leftTotalExposedSize + this.exposedSize)) || + ((affinity == PositionAffinity.Successor) && (exposedPosition < leftTotalExposedSize + this.exposedSize))) + { + return new SnapshotPoint(sourceSnapshot, + sourcePrefixLength + LeftTotalSourceSize() + (exposedPosition - leftTotalExposedSize)); + } + + else if (right == null) + { + // we are mapping the last exposedPosition in the buffer, that is, the one that is just past the end. + // ignore the affinity. + Debug.Assert(exposedPosition == leftTotalExposedSize + this.exposedSize); + return new SnapshotPoint(sourceSnapshot, sourcePrefixLength + LeftTotalSourceSize() + this.exposedSize); + } + + else + { + return this.right.MapToSourceSnapshot(sourceSnapshot : sourceSnapshot, + exposedPosition : exposedPosition - (leftTotalExposedSize + this.exposedSize), + sourcePrefixLength : sourcePrefixLength + LeftTotalSourceSize() + this.sourceSize, + affinity : affinity); + } + } + + public SnapshotPoint? MapFromSourceSnapshot(ITextSnapshot snapshot, int sourcePosition, int exposedPrefixLength) + { + Debug.Assert(sourcePosition <= this.totalSourceSize); + int leftTotalSourceSize = LeftTotalSourceSize(); + + if (sourcePosition < leftTotalSourceSize) + { + if (left == null) + { + return null; + } + else + { + return this.left.MapFromSourceSnapshot(snapshot, sourcePosition, exposedPrefixLength); + } + } + else if (sourcePosition < leftTotalSourceSize + this.sourceSize) + { + if ((sourcePosition <= leftTotalSourceSize + this.exposedSize) && (!this.leftmostElision)) + { + return new SnapshotPoint(snapshot, + exposedPrefixLength + LeftTotalExposedSize() + (sourcePosition - leftTotalSourceSize)); + } + else + { + return null; + } + } + else if (right == null) + { + // We are mapping the last position in the source snapshot + if (this.exposedSize < this.sourceSize) + { + return null; + } + else + { + return new SnapshotPoint(snapshot, exposedPrefixLength + LeftTotalExposedSize() + this.exposedSize); + } + } + else + { + return this.right.MapFromSourceSnapshot(snapshot, + sourcePosition - (leftTotalSourceSize + this.sourceSize), + exposedPrefixLength + LeftTotalExposedSize() + this.exposedSize); + } + } + + public int MapFromSourceSnapshotToNearest(int sourcePosition, int exposedPrefixLength) + { + Debug.Assert(sourcePosition <= this.totalSourceSize); + + if (sourcePosition < LeftTotalSourceSize()) + { + return this.left.MapFromSourceSnapshotToNearest(sourcePosition, exposedPrefixLength); + } + else + { + // shift coordinates to start with current node + exposedPrefixLength += LeftTotalExposedSize(); + sourcePosition -= LeftTotalSourceSize(); + if (sourcePosition < this.sourceSize) + { + if (sourcePosition < this.exposedSize) + { + // position is in the exposed segment + return exposedPrefixLength + sourcePosition; + } + else + { + // position is in the hidden segment + return exposedPrefixLength + this.exposedSize; + } + } + else if (right == null) + { + // We are mapping the last position in the source snapshot + return exposedPrefixLength + this.exposedSize; + } + else + { + // shift coordinates to right subtree + exposedPrefixLength += this.exposedSize; + sourcePosition -= this.sourceSize; + return this.right.MapFromSourceSnapshotToNearest(sourcePosition, exposedPrefixLength); + } + } + } + + /// <summary> + /// Map a span over the exposed text to spans on the source buffer. + /// </summary> + /// <param name="sourceSnapshot">Snapshot of the source buffer to which to map.</param> + /// <param name="mapSpan">The span to map, expressed in terms of exposed text.</param> + /// <param name="sourcePrefixSize">The amount of source text (hidden or exposed) that precedes the text in this subtree.</param> + /// <param name="result">List of spans to which to append the result, expressed in terms of source text.</param> + public void MapToSourceSnapshots(ITextSnapshot sourceSnapshot, + Span mapSpan, + int sourcePrefixSize, + FrugalList<SnapshotSpan> result) + { + // This method is written for maximum clarity + + int leftTotalExposedSize = LeftTotalExposedSize(); + Span leftSpan = new Span(0, leftTotalExposedSize); + Span midSpan = new Span(leftTotalExposedSize, this.exposedSize); + Span rightSpan = new Span(leftTotalExposedSize + this.exposedSize, RightTotalExposedSize()); + + // map the requested span to the left subtree, this node, and the right subtree, in terms of exposed text + Span? leftMapSpan = mapSpan.Overlap(leftSpan); + Span? midMapSpan = mapSpan.Overlap(midSpan); + Span? rightMapSpan = mapSpan.Overlap(rightSpan); + + if (leftMapSpan != null) + { + this.left.MapToSourceSnapshots(sourceSnapshot, + leftMapSpan.Value, + sourcePrefixSize, + result); + } + + if (midMapSpan != null) + { + // the source span for the current node has the same length as the midMapSpan, but the start is + // adjusted by the total amount of source text to the left of this node, which is the sourcePrefixSize plus + // the amount of text hidden in the left subtree. + Span sourceSpan = new Span(midMapSpan.Value.Start + sourcePrefixSize + LeftTotalHiddenSize(), midMapSpan.Value.Length); + result.Add(new SnapshotSpan(sourceSnapshot, sourceSpan)); + } + + if (rightMapSpan != null) + { + this.right.MapToSourceSnapshots(sourceSnapshot, + new Span(rightMapSpan.Value.Start - leftTotalExposedSize - this.exposedSize, rightMapSpan.Value.Length), + sourcePrefixSize + LeftTotalSourceSize() + this.sourceSize, + result); + } + } + + public void MapFromSourceSnapshot(Span mapSpan, int exposedPrefixSize, FrugalList<Span> result) + { + int leftTotalSourceSize = LeftTotalSourceSize(); + Span leftSourceSpan = new Span(0, leftTotalSourceSize); + Span midExposedSpan = new Span(leftTotalSourceSize, this.exposedSize); + Span rightSourceSpan = new Span(leftTotalSourceSize + this.sourceSize, RightTotalSourceSize()); + + // map the requested span to the left subtree, this node, and the right subtree, in terms of source text + Span? leftMapSpan = mapSpan.Overlap(leftSourceSpan); + Span? midMapSpan = mapSpan.Overlap(midExposedSpan); + Span? rightMapSpan = mapSpan.Overlap(rightSourceSpan); + + if (leftMapSpan != null) + { + this.left.MapFromSourceSnapshot(leftMapSpan.Value, exposedPrefixSize, result); + } + + if (midMapSpan != null) + { + result.Add(new Span(midMapSpan.Value.Start + exposedPrefixSize - LeftTotalHiddenSize(), midMapSpan.Value.Length)); + } + + if (rightMapSpan != null) + { + this.right.MapFromSourceSnapshot(new Span(rightMapSpan.Value.Start - leftTotalSourceSize - this.sourceSize, rightMapSpan.Value.Length), + exposedPrefixSize + LeftTotalExposedSize() + this.exposedSize, + result); + } + } + + public void MapNullSpanFromSourceSnapshot(Span nullSourceSpan, int exposedPrefixSize, FrugalList<Span> result) + { + int leftTotalSourceSize = LeftTotalSourceSize(); + Span midExposedSpan = new Span(leftTotalSourceSize, this.exposedSize); + + if (this.left != null) + { + Span leftSourceSpan = new Span(0, leftTotalSourceSize); + if (leftSourceSpan.IntersectsWith(nullSourceSpan)) + { + this.left.MapNullSpanFromSourceSnapshot(nullSourceSpan, exposedPrefixSize, result); + } + } + + if (!this.leftmostElision && midExposedSpan.IntersectsWith(nullSourceSpan)) + { + Span intersection = midExposedSpan.Intersection(nullSourceSpan).Value; + result.Add(new Span(intersection.Start + exposedPrefixSize - LeftTotalHiddenSize(), 0)); + } + + if (this.right != null) + { + Span rightSourceSpan = new Span(leftTotalSourceSize + this.sourceSize, RightTotalSourceSize()); + if (rightSourceSpan.IntersectsWith(nullSourceSpan)) + { + Span intersection = rightSourceSpan.Intersection(nullSourceSpan).Value; + this.right.MapNullSpanFromSourceSnapshot(new Span(intersection.Start - leftTotalSourceSize - this.sourceSize, 0), + exposedPrefixSize + LeftTotalExposedSize() + this.exposedSize, + result); + } + } + } + + public void MapInsertionPointToSourceSnapshots(IElisionSnapshot elisionSnapshot, + int exposedPosition, + int sourcePrefixLength, + FrugalList<SnapshotPoint> points) + { + Debug.Assert(exposedPosition >= 0); + Debug.Assert(exposedPosition <= this.totalExposedSize); + + int leftTotalExposedSize = LeftTotalExposedSize(); + + if (this.left != null && exposedPosition <= leftTotalExposedSize) + { + this.left.MapInsertionPointToSourceSnapshots(elisionSnapshot, + exposedPosition, + sourcePrefixLength, + points); + } + + bool ignoreThisNode = this.leftmostElision && elisionSnapshot.Length > 0; + + if (!ignoreThisNode) + { + if (exposedPosition >= leftTotalExposedSize && exposedPosition <= leftTotalExposedSize + this.exposedSize) + { + points.Add(new SnapshotPoint(elisionSnapshot.SourceSnapshot, sourcePrefixLength + LeftTotalSourceSize() + (exposedPosition - leftTotalExposedSize))); + } + } + + if (this.right != null && exposedPosition >= leftTotalExposedSize + this.exposedSize) + { + this.right.MapInsertionPointToSourceSnapshots(elisionSnapshot, + exposedPosition - (LeftTotalExposedSize() + this.exposedSize), + sourcePrefixLength + LeftTotalSourceSize() + this.sourceSize, + points); + } + } + #endregion + + #region Lines + + //private void TraceEnter(int level) + //{ + // Debug.Write(new string(' ', level * 3)); + // Debug.Write(this.exposedSize); + // Debug.Write(' '); + // Debug.WriteLine(this.sourceSize); + //} + + //private static void TraceExit(int level, LineInfo info) + //{ + // Debug.Write(new string(' ', level * 3)); + // Debug.WriteLine(info.ToString()); + //} + + public ProjectionLineInfo GetLineFromPosition(ITextSnapshot sourceSnapshot, + int exposedPosition, + int sourcePrefixLineBreakCount, + int hiddenPrefixLineBreakCount, + int sourcePrefixSize, + int exposedPrefixSize, + int level) + { + //TraceEnter(level); + Span relativeExposedSpan = new Span(LeftTotalExposedSize(), this.exposedSize); + ProjectionLineCalculationState state = ProjectionLineCalculationState.Primary; + int relativeExposedPosition = exposedPosition; + ProjectionLineInfo pendingInfo = new ProjectionLineInfo(); + + do + { + if (relativeExposedPosition < relativeExposedSpan.Start) + { + #region relative ExposedPosition is in left subtree + Debug.Assert(state != ProjectionLineCalculationState.Append); + + // recursively compute that part of the line that is in the left subtree + ProjectionLineInfo leftInfo = this.left.GetLineFromPosition + (sourceSnapshot : sourceSnapshot, + exposedPosition : relativeExposedPosition, + sourcePrefixLineBreakCount : sourcePrefixLineBreakCount, + hiddenPrefixLineBreakCount : hiddenPrefixLineBreakCount, + sourcePrefixSize : sourcePrefixSize, + exposedPrefixSize : exposedPrefixSize, + level : level + 1); + + if (state == ProjectionLineCalculationState.Primary) + { + if (leftInfo.endComplete) + { + // leftInfo.startComplete may be false, but we aren't going to find anything + // further to the left at this level of the tree, so we are done here + // TraceExit(level, leftInfo); + return leftInfo; + } + else + { + // the end of the line extends into the current node. our new position + // of interest is the start of the exposed text in this node. + state = ProjectionLineCalculationState.Append; + pendingInfo = leftInfo; + relativeExposedPosition = relativeExposedSpan.Start; + continue; + } + } + else + { + // We've just been looking for the beginning of the line in the left subtree + Debug.Assert(state == ProjectionLineCalculationState.Prepend || state == ProjectionLineCalculationState.Bipend); + if (pendingInfo.lineNumber == leftInfo.lineNumber) + { + // the left node contained more of the line we are looking for + // we may or may not have found the start of the line, but there + // is no more to find at this level + pendingInfo.start = leftInfo.start; + pendingInfo.startComplete = leftInfo.startComplete; + } + else + { + // the left exposed source ended with a line break, so + // there is no change to the previously computed start + pendingInfo.startComplete = true; + } + if (state == ProjectionLineCalculationState.Bipend) + { + // now we need to look in the right subtree + state = ProjectionLineCalculationState.Append; + relativeExposedPosition = relativeExposedSpan.End; + continue; + } + else + { + // TraceExit(level, pendingInfo); + return pendingInfo; + } + } + #endregion + } + else if (relativeExposedPosition < relativeExposedSpan.End || this.right == null) + { + #region relative ExposedPosition is in current node + int absoluteSourcePosition = sourcePrefixSize + LeftTotalHiddenSize() + relativeExposedPosition; + ITextSnapshotLine sourceLine = sourceSnapshot.GetLineFromPosition(absoluteSourcePosition); + ProjectionLineCalculationState nextState = ProjectionLineCalculationState.Primary; + int provisionalLineNumber = sourceLine.LineNumber - (LeftTotalHiddenLineBreakCount() + hiddenPrefixLineBreakCount); + + if (state == ProjectionLineCalculationState.Primary) + { + pendingInfo = new ProjectionLineInfo(); + + // the primary position we are searching for is in the current node. + // now we know the line number! + pendingInfo.lineNumber = provisionalLineNumber; + Debug.Assert(pendingInfo.lineNumber >= 0); + } + + // compute the length of the portion of the source line that precedes the position we've been searching for + int sourceLeader = absoluteSourcePosition - sourceLine.Start; + // where would that map to in the current node? + int relativeExposedLineStart = relativeExposedPosition - sourceLeader; + + if (state == ProjectionLineCalculationState.Prepend && provisionalLineNumber < pendingInfo.lineNumber) + { + // we were trying to pick up the beginning of a line that had been elided, but it was + // elided all the way to its beginning, so we are done. + pendingInfo.startComplete = true; + } + else if (state == ProjectionLineCalculationState.Primary || state == ProjectionLineCalculationState.Prepend) + { + // if we are lucky, the whole line will be contained in this node + + if (relativeExposedLineStart > relativeExposedSpan.Start) + { + // we are sure that nothing to our left is of interest + pendingInfo.start = exposedPrefixSize + relativeExposedLineStart; + pendingInfo.startComplete = true; + } + else + { + // rats! part of the 'leader' is elided. + // start with the beginning of this segment + pendingInfo.start = exposedPrefixSize + relativeExposedSpan.Start; + pendingInfo.startComplete = false; + // and check further to the left if there is anything there + if (LeftTotalExposedSize() > 0) + { + nextState = ProjectionLineCalculationState.Prepend; + relativeExposedPosition = relativeExposedSpan.Start - 1; + } + } + } + + if (state == ProjectionLineCalculationState.Primary || state == ProjectionLineCalculationState.Append) + { + int exposedLineEnd = relativeExposedLineStart + sourceLine.LengthIncludingLineBreak; + if (exposedLineEnd <= relativeExposedSpan.End) + { + // good! + pendingInfo.end = exposedLineEnd + exposedPrefixSize - sourceLine.LineBreakLength; + pendingInfo.endComplete = true; + pendingInfo.lineBreakLength = sourceLine.LineBreakLength; + } + else + { + pendingInfo.end = exposedPrefixSize + relativeExposedSpan.End; + pendingInfo.endComplete = false; + if (this.right == null) + { + if (nextState != ProjectionLineCalculationState.Prepend) + { + // we need to go further right but there is nothing more below us + // TraceExit(level, pendingInfo); + return pendingInfo; + } + } + else + { + if (nextState == ProjectionLineCalculationState.Prepend) + { + nextState = ProjectionLineCalculationState.Bipend; + } + else + { + nextState = ProjectionLineCalculationState.Append; + relativeExposedPosition = relativeExposedSpan.End; + } + } + } + } + if (nextState == ProjectionLineCalculationState.Primary) + { + // TraceExit(level, pendingInfo); + return pendingInfo; + } + state = nextState; + #endregion + } + else + { + #region relative ExposedPosition is in right subtree + Debug.Assert(state != ProjectionLineCalculationState.Bipend); + + // recursively compute that part of the line that is in the right subtree + ProjectionLineInfo rightInfo = this.right.GetLineFromPosition + (sourceSnapshot : sourceSnapshot, + exposedPosition : relativeExposedPosition - (LeftTotalExposedSize() + this.exposedSize), + sourcePrefixLineBreakCount : sourcePrefixLineBreakCount + LeftTotalSourceLineBreakCount() + this.sourceLineBreakCount, + hiddenPrefixLineBreakCount : hiddenPrefixLineBreakCount + LeftTotalHiddenLineBreakCount() + (this.sourceLineBreakCount - this.exposedLineBreakCount), + sourcePrefixSize : sourcePrefixSize + LeftTotalSourceSize() + this.sourceSize, + exposedPrefixSize : exposedPrefixSize + LeftTotalExposedSize() + this.exposedSize, + level : level + 1); + if (state == ProjectionLineCalculationState.Primary) + { + if (rightInfo.startComplete) + { + // rightInfo.endComplete may be false, but we aren't going to find anything + // further to the right at this level of three, so we are done here + // TraceExit(level, rightInfo); + return rightInfo; + } + else + { + // the begnning of the line extends into the current node. our new position + // of interest is the end of the exposed text in this node. + state = ProjectionLineCalculationState.Prepend; + pendingInfo = rightInfo; + relativeExposedPosition = relativeExposedSpan.End - 1; + continue; + } + } + else + { + // We've just been looking for the end of the line in the right subtree + Debug.Assert(state == ProjectionLineCalculationState.Append); + + // the first line we saw in the right subtree must have been the same line + // since there wasn't a line break at the end of the current node + Debug.Assert(pendingInfo.lineNumber == rightInfo.lineNumber); + + pendingInfo.end = rightInfo.end; + pendingInfo.endComplete = rightInfo.endComplete; + pendingInfo.lineBreakLength = rightInfo.lineBreakLength; + // TraceExit(level, pendingInfo); + return pendingInfo; + } + #endregion + } + } while (relativeExposedPosition >= 0 && relativeExposedPosition <= this.totalExposedSize); + + // TraceExit(level, pendingInfo); + return pendingInfo; + } + + public ProjectionLineInfo GetLineFromLineNumber(ITextSnapshot sourceSnapshot, + int exposedLineNumber) + { + int pos = GetPositionFromLineNumber(sourceSnapshot, exposedLineNumber, 0, 0); + ProjectionLineInfo info = this.GetLineFromPosition(sourceSnapshot, pos, 0, 0, 0, 0, 0); + Debug.Assert(info.lineNumber == exposedLineNumber); + return info; + } + + private int GetPositionFromLineNumber(ITextSnapshot sourceSnapshot, + int relativeExposedLineNumber, + int sourcePrefixLineBreakCount, + int sourcePrefixHiddenSize) + + // return the absolute (exposed) position of the (first) line break character in the requested line + { + if (relativeExposedLineNumber < LeftTotalExposedLineBreakCount()) + { + return this.left.GetPositionFromLineNumber + (sourceSnapshot : sourceSnapshot, + relativeExposedLineNumber : relativeExposedLineNumber, + sourcePrefixLineBreakCount : sourcePrefixLineBreakCount, + sourcePrefixHiddenSize : sourcePrefixHiddenSize); + } + + else if (relativeExposedLineNumber < LeftTotalExposedLineBreakCount() + this.exposedLineBreakCount || this.right == null) + { + int absoluteSourceLineNumber = sourcePrefixLineBreakCount + LeftTotalHiddenLineBreakCount() + relativeExposedLineNumber; + ITextSnapshotLine sourceLine = sourceSnapshot.GetLineFromLineNumber(absoluteSourceLineNumber); + return sourceLine.End - sourcePrefixHiddenSize - LeftTotalHiddenSize(); + } + + else + { + return this.right.GetPositionFromLineNumber + (sourceSnapshot : sourceSnapshot, + relativeExposedLineNumber : relativeExposedLineNumber - (LeftTotalExposedLineBreakCount() + this.exposedLineBreakCount), + sourcePrefixLineBreakCount : sourcePrefixLineBreakCount + LeftTotalSourceLineBreakCount() + this.sourceLineBreakCount, + sourcePrefixHiddenSize : sourcePrefixHiddenSize + LeftTotalHiddenSize() + (this.sourceSize - this.exposedSize)); + } + } + + public int GetLineNumberFromPosition(ITextSnapshot sourceSnapshot, int exposedPosition, int hiddenPrefixLineBreakCount, int sourcePrefixSize) + { + Span midSpan = new Span(LeftTotalExposedSize(), this.exposedSize); + + if (exposedPosition < midSpan.Start) + { + return this.left.GetLineNumberFromPosition + (sourceSnapshot : sourceSnapshot, + exposedPosition : exposedPosition, + hiddenPrefixLineBreakCount : hiddenPrefixLineBreakCount, + sourcePrefixSize : sourcePrefixSize); + } + else if ((exposedPosition < midSpan.End) || this.right == null) + { + // if we took this branch because this.right is null, the position is the last position in the elision buffer + return sourceSnapshot.GetLineNumberFromPosition(exposedPosition + sourcePrefixSize + LeftTotalSourceSize() - LeftTotalExposedSize()) - + (LeftTotalHiddenLineBreakCount() + hiddenPrefixLineBreakCount); + } + else + { + return this.right.GetLineNumberFromPosition + (sourceSnapshot : sourceSnapshot, + exposedPosition : exposedPosition - midSpan.End, + hiddenPrefixLineBreakCount : hiddenPrefixLineBreakCount + LeftTotalHiddenLineBreakCount() + (this.sourceLineBreakCount - this.exposedLineBreakCount), + sourcePrefixSize : sourcePrefixSize + LeftTotalSourceSize() + this.sourceSize); + } + } + #endregion + + #region Change Incorporation + /// <summary> + /// Incorporate a text change in the source buffer into the elision map. + /// </summary> + /// <param name="beforeSourceSnapshot">Snapshot of the source buffer before the change occurred.</param> + /// <param name="beforeElisionSnapshot">Snapshot of the elision buffer before the change occurred.</param> + /// <param name="sourceInsertionPosition">If there is an insertion as part of the change, the position of that + /// insertion relative to the subtree rooted at this node; otherwise null.</param> + /// <param name="newText">New text to be inserted (or the null string).</param> + /// <param name="sourceDeletionSpan">If there is a deletion as part of the change, the span of that + /// deletion relative to the subtree rooted at this node; otherwise null.</param> + /// <param name="absoluteSourceOldPosition">The absolute position of the change in the <paramref name="beforeSourceSnapshot"/>.</param> + /// <param name="projectedPrefixSize">The size (in characters) of that portion of elision buffer that + /// precedes this node and is not in its left subtree.</param> + /// <param name="projectedChanges">List of changes projected into the elision buffer; constructed by + /// this function.</param> + /// <param name="incomingAccumulatedDelta">Accumulated delta from prior source changes in the current + /// multipart edit transaction.</param> + /// <param name="outgoingAccumulatedDelta">Increment to the accumulated delta resulting from the + /// current change.</param> + /// <param name="accumulatedDelete">Amount of text deleted so far for the current change.</param> + /// <returns>A new immutable ElisionMapNode.</returns> + public ElisionMapNode IncorporateChange(ITextSnapshot beforeSourceSnapshot, + ITextSnapshot afterSourceSnapshot, + ITextSnapshot beforeElisionSnapshot, + int? sourceInsertionPosition, + StringRebuilder newText, + Span? sourceDeletionSpan, + int absoluteSourceOldPosition, + int absoluteSourceNewPosition, + int projectedPrefixSize, + FrugalList<TextChange> projectedChanges, + int incomingAccumulatedDelta, + ref int outgoingAccumulatedDelta, + ref int accumulatedDelete) + { + // All positions and spans in this method are relative to the current subtree except those with 'absolute' in the name + // All positions and spans in coordinate space of source buffer have 'source' in the name + // All positions and spans in coordinate space of elision buffer have 'projected' in the name + + Debug.Assert(sourceDeletionSpan == null || sourceDeletionSpan.Value.End <= this.totalSourceSize); + Debug.Assert(sourceInsertionPosition == null || sourceInsertionPosition >= 0); + Debug.Assert(sourceInsertionPosition == null || sourceInsertionPosition <= this.totalSourceSize); + + ElisionMapNode newLeft = this.left; + ElisionMapNode newRight = this.right; + int newExposedLineBreakCount = this.exposedLineBreakCount; + int newSourceLineBreakCount = this.sourceLineBreakCount; + int newExposedSize = exposedSize; + int newSourceSize = sourceSize; + bool newLeftmostElision = this.leftmostElision; + + int leftTotalSourceSize = LeftTotalSourceSize(); + + Span leftSourceSpan = new Span(0, leftTotalSourceSize); + Span midExposedSourceSpan = new Span(leftTotalSourceSize, this.exposedSize); + Span midHiddenSourceSpan = new Span(midExposedSourceSpan.End, this.sourceSize - this.exposedSize); + Span rightSourceSpan = new Span(midHiddenSourceSpan.End, this.totalSourceSize - leftTotalSourceSize - this.sourceSize); + + #region Incorporate left subtree changes + Span? leftSourceDeletionSpan = leftSourceSpan.Overlap(sourceDeletionSpan); + bool insertionOnLeft = sourceInsertionPosition.HasValue && sourceInsertionPosition.Value < leftTotalSourceSize; + int? leftSourceInsertionPosition = null; + if (insertionOnLeft) + { + if (leftSourceDeletionSpan.HasValue && leftSourceDeletionSpan.Value.End == midExposedSourceSpan.Start) + { + // we have a replacement, and the deleted text in the left subtree touches the left edge of the current node. + // The inserted text needs to be exposed in this node, not possibly swallowed into a hidden part of the left node. + // Leave the leftSourceInsertionPosition equal to null so the left subtree doesn't stick it at the end of itself + insertionOnLeft = false; + sourceInsertionPosition = midExposedSourceSpan.Start; + Debug.Assert(!this.leftmostElision); // there is no node to the left of a leftmost elision node + } + else + { + leftSourceInsertionPosition = sourceInsertionPosition; + } + } + if (insertionOnLeft || leftSourceDeletionSpan.HasValue) + { + // insertion (if any) and start of deletion (if any) is in the left subtree + newLeft = this.left.IncorporateChange(beforeSourceSnapshot : beforeSourceSnapshot, + afterSourceSnapshot : afterSourceSnapshot, + beforeElisionSnapshot : beforeElisionSnapshot, + sourceInsertionPosition : leftSourceInsertionPosition, + newText : newText, + sourceDeletionSpan : leftSourceSpan.Overlap(sourceDeletionSpan), + absoluteSourceOldPosition : absoluteSourceOldPosition, + absoluteSourceNewPosition : absoluteSourceNewPosition, + projectedPrefixSize : projectedPrefixSize, + projectedChanges : projectedChanges, + incomingAccumulatedDelta : incomingAccumulatedDelta, + outgoingAccumulatedDelta : ref outgoingAccumulatedDelta, + accumulatedDelete : ref accumulatedDelete); + } + #endregion + + #region Incorporate current subtree changes + + Span? exposedSourceDeletion = midExposedSourceSpan.Overlap(sourceDeletionSpan); + Span? hiddenSourceDeletion = midHiddenSourceSpan.Overlap(sourceDeletionSpan); + + // Insertion and deletion are handled independently and recombined by text change normalization + // Insertion + + // In each of the three cases below, if an insertion belongs in this node, set sourceInsertionPosition to + // null, preventing it from also being performed in the right subtree when the current node has size zero. + if (sourceInsertionPosition.HasValue) + { + // special case for leftmostElision node + if (this.leftmostElision) + { + // insertion into the exposed part of this node isn't possible + Debug.Assert(this.left == null); + Debug.Assert(this.exposedSize == 0); + Debug.Assert(leftTotalSourceSize == 0); + if (sourceInsertionPosition.Value <= this.sourceSize) + { + // insertion into hidden prefix of elision buffer + newSourceSize += newText.Length; + + int incrementalLineCount; + ComputeIncrementalLineCountForHiddenInsertion(afterSourceSnapshot, absoluteSourceNewPosition, newText, out incrementalLineCount); + + newSourceLineBreakCount += incrementalLineCount; + sourceInsertionPosition = null; + } + } + else if ((leftTotalSourceSize <= sourceInsertionPosition.Value) && + (sourceInsertionPosition.Value <= leftTotalSourceSize + this.exposedSize)) + { + // insertion (if any) is in the exposed part of the current node + newExposedSize += newText.Length; + newSourceSize += newText.Length; + int projectedPosition = projectedPrefixSize + sourceInsertionPosition.Value - LeftTotalHiddenSize() - incomingAccumulatedDelta; + + // effects on line count are computed based on local information. Interactions with adjacent segments + // must be handled at a higher level (undone). + + int deletionLength = 0; + if (exposedSourceDeletion.HasValue) + { + Debug.Assert(exposedSourceDeletion.Value.Start == sourceInsertionPosition.Value); + deletionLength = exposedSourceDeletion.Value.Length; + } + + char? predChar = sourceInsertionPosition.Value > midExposedSourceSpan.Start + ? afterSourceSnapshot[absoluteSourceNewPosition - 1] + : (char?)null; // insertion is at start of segment + char? succChar = sourceInsertionPosition.Value + deletionLength < midExposedSourceSpan.End + ? afterSourceSnapshot[absoluteSourceNewPosition + newText.Length] + : (char?)null; // insertion is at end of segment + + LineBreakBoundaryConditions boundaryConditions; + int incrementalLineCount; + ComputeIncrementalLineCountForExposedInsertion(predChar, succChar, newText, out incrementalLineCount, out boundaryConditions); + + newExposedLineBreakCount += incrementalLineCount; + newSourceLineBreakCount += incrementalLineCount; + + TextChange change = new TextChange(projectedPosition, StringRebuilder.Empty, newText, boundaryConditions); + projectedChanges.Add(change); + outgoingAccumulatedDelta += change.Delta; + sourceInsertionPosition = null; + } + else if ((leftTotalSourceSize + this.exposedSize < sourceInsertionPosition.Value) && + ((sourceInsertionPosition.Value < leftTotalSourceSize + this.sourceSize) || (this.right == null))) + { + // insertion (if any) is in the hidden part of the current node + // Unless...we are also deleting from the point of the insertion through the end of the node, in which case + // the insertion should go at the beginning of the next segment. If there is no right subtree, this case will + // have been handled on the way down by some ancestor node. + if (this.right != null && (hiddenSourceDeletion.HasValue && hiddenSourceDeletion.Value.End == midHiddenSourceSpan.End)) + { + sourceInsertionPosition = midHiddenSourceSpan.End; + } + else + { + newSourceSize += newText.Length; + + int incrementalLineCount; + ComputeIncrementalLineCountForHiddenInsertion(afterSourceSnapshot, absoluteSourceNewPosition, newText, out incrementalLineCount); + + newSourceLineBreakCount += incrementalLineCount; + sourceInsertionPosition = null; + } + } + } + + // Deletion of exposed text + if (exposedSourceDeletion.HasValue) + { + newExposedSize -= exposedSourceDeletion.Value.Length; + newSourceSize -= exposedSourceDeletion.Value.Length; + int projectedDeletionPosition = projectedPrefixSize + exposedSourceDeletion.Value.Start - LeftTotalHiddenSize(); + int sourceDeletionSegmentPosition = absoluteSourceOldPosition - accumulatedDelete; + StringRebuilder exposedDeletionText = + BufferFactoryService.StringRebuilderFromSnapshotAndSpan(beforeSourceSnapshot, new Span(sourceDeletionSegmentPosition, exposedSourceDeletion.Value.Length)); + + LineBreakBoundaryConditions boundaryConditions; + int incrementalLineCount; + ComputeIncrementalLineCountForDeletion(beforeElisionSnapshot, new Span(projectedDeletionPosition - incomingAccumulatedDelta, exposedSourceDeletion.Value.Length), exposedDeletionText, out incrementalLineCount, out boundaryConditions); + + newExposedLineBreakCount += incrementalLineCount; + newSourceLineBreakCount += incrementalLineCount; + TextChange change = new TextChange(projectedDeletionPosition - incomingAccumulatedDelta, exposedDeletionText, StringRebuilder.Empty, boundaryConditions); + projectedChanges.Add(change); + outgoingAccumulatedDelta += change.Delta; + accumulatedDelete += change.Delta; + } + + // Deletion of hidden text + if (hiddenSourceDeletion.HasValue) + { + int sourceDeletionSegmentPosition = absoluteSourceOldPosition - accumulatedDelete; + StringRebuilder hiddenDeletionText = + BufferFactoryService.StringRebuilderFromSnapshotAndSpan(beforeSourceSnapshot, new Span(sourceDeletionSegmentPosition, hiddenSourceDeletion.Value.Length)); + + LineBreakBoundaryConditions dontCare; + int incrementalLineCount; + ComputeIncrementalLineCountForDeletion(beforeSourceSnapshot, new Span(sourceDeletionSegmentPosition, hiddenSourceDeletion.Value.Length), hiddenDeletionText, out incrementalLineCount, out dontCare); + newSourceLineBreakCount += incrementalLineCount; + + newSourceSize -= hiddenSourceDeletion.Value.Length; + accumulatedDelete -= hiddenSourceDeletion.Value.Length; + } + #endregion + + #region Incorporate right subtree changes + Span? rightSourceDeletionSpan = rightSourceSpan.Overlap(sourceDeletionSpan); + bool insertionOnRight = (sourceInsertionPosition.HasValue) && (this.right != null) && (leftTotalSourceSize + this.sourceSize <= sourceInsertionPosition.Value); + if (rightSourceDeletionSpan.HasValue || insertionOnRight) + { + // insertion (if any) or part of deletion (if any) is in the right subtree + newRight = this.right.IncorporateChange(beforeSourceSnapshot : beforeSourceSnapshot, + afterSourceSnapshot : afterSourceSnapshot, + beforeElisionSnapshot : beforeElisionSnapshot, + sourceInsertionPosition : insertionOnRight ? sourceInsertionPosition.Value - leftTotalSourceSize - this.sourceSize : (int?)null, + newText : insertionOnRight ? newText : StringRebuilder.Empty, + sourceDeletionSpan : rightSourceDeletionSpan.HasValue + ? new Span(rightSourceDeletionSpan.Value.Start - (leftTotalSourceSize + this.sourceSize), + rightSourceDeletionSpan.Value.Length) + : (Span?)null, + absoluteSourceOldPosition : absoluteSourceOldPosition, + absoluteSourceNewPosition : absoluteSourceNewPosition, + projectedPrefixSize : projectedPrefixSize + LeftTotalExposedSize() + this.exposedSize, + projectedChanges : projectedChanges, + incomingAccumulatedDelta : incomingAccumulatedDelta, + outgoingAccumulatedDelta : ref outgoingAccumulatedDelta, + accumulatedDelete : ref accumulatedDelete); + } + #endregion + + return new ElisionMapNode(newExposedSize, newSourceSize, newExposedLineBreakCount, newSourceLineBreakCount, newLeft, newRight, newLeftmostElision); + } + + private static void ComputeIncrementalLineCountForHiddenInsertion(ITextSnapshot afterSnapshot, + int start, + StringRebuilder insertedText, + out int incrementalLineCount) + { + int lineCount = insertedText.LineBreakCount; + LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None; + + Debug.Assert(start >= 0 && start <= afterSnapshot.Length); + Debug.Assert(insertedText.Length > 0); + + if (start > 0 && afterSnapshot[start - 1] == '\r') + { + bc = bc | LineBreakBoundaryConditions.PrecedingReturn; + if (insertedText.FirstCharacter == '\n') + { + // \n was inserted after \r, and we counted it above as a new line, which it isn't. + // correct for that. + lineCount--; + } + } + + int end = start + insertedText.Length; + if (end < afterSnapshot.Length && afterSnapshot[end] == '\n') + { + bc = bc | LineBreakBoundaryConditions.SucceedingNewline; + if (insertedText.LastCharacter == '\r') + { + // \r was inserted before \n, and we counted it above as a new line, which it isn't. + // correct for that. + lineCount--; + } + } + if (bc == (LineBreakBoundaryConditions.PrecedingReturn | LineBreakBoundaryConditions.SucceedingNewline)) + { + // the insertion separated a \r and a \n, so increase the line count + lineCount++; + } + incrementalLineCount = lineCount; + } + + /// <summary> + /// Determine the impact of an insertion on the line count. + /// </summary> + private static void ComputeIncrementalLineCountForExposedInsertion(char? predecessor, + char? successor, + StringRebuilder insertedText, + out int incrementalLineCount, + out LineBreakBoundaryConditions boundaryConditions) + { + int lineCount = insertedText.LineBreakCount; + LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None; + + if (predecessor == '\r') + { + bc = bc | LineBreakBoundaryConditions.PrecedingReturn; + if (insertedText.FirstCharacter == '\n') + { + // \n was inserted after \r, and we counted it above as a new line, which it isn't. + // correct for that. + lineCount--; + } + } + if (successor == '\n') + { + bc = bc | LineBreakBoundaryConditions.SucceedingNewline; + if (insertedText.LastCharacter == '\r') + { + // \r was inserted before \n, and we counted it above as a new line, which it isn't. + // correct for that. + lineCount--; + } + } + if (bc == (LineBreakBoundaryConditions.PrecedingReturn | LineBreakBoundaryConditions.SucceedingNewline)) + { + // the insertion separated a \r and a \n, so increase the line count + lineCount++; + } + incrementalLineCount = lineCount; + boundaryConditions = bc; + } + + /// <summary> + /// Determine the impact of a deletion on the line count. + /// </summary> + private static void ComputeIncrementalLineCountForDeletion(ITextSnapshot beforeSnapshot, + Span deletionSpan, + StringRebuilder deletedText, + out int incrementalLineCount, + out LineBreakBoundaryConditions boundaryConditions) + { + int lineCount = -deletedText.LineBreakCount; + LineBreakBoundaryConditions bc = LineBreakBoundaryConditions.None; + + Debug.Assert(deletionSpan.End <= beforeSnapshot.Length); + Debug.Assert(deletedText.Length > 0); + + if (deletionSpan.Start > 0 && beforeSnapshot[deletionSpan.Start - 1] == '\r') + { + bc = bc | LineBreakBoundaryConditions.PrecedingReturn; + if (deletedText[0] == '\n') + { + // the \n of a \r\n pair was deleted and we counted it as losing a line, which it isn't. + // correct for that. + lineCount++; + } + } + if (deletionSpan.End < beforeSnapshot.Length && beforeSnapshot[deletionSpan.End] == '\n') + { + bc = bc | LineBreakBoundaryConditions.SucceedingNewline; + if (deletedText[deletedText.Length - 1] == '\r') + { + // the \r of a \r\n pair was deleted and we counted it as losing a line, which it isn't. + // correct for that. + lineCount++; + } + } + if (bc == (LineBreakBoundaryConditions.PrecedingReturn | LineBreakBoundaryConditions.SucceedingNewline)) + { + // the deletion joined a \r and a \n, so decrease the line count + lineCount--; + } + incrementalLineCount = lineCount; + boundaryConditions = bc; + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs b/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs new file mode 100644 index 0000000..36b0779 --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/ElisionSnapshot.cs @@ -0,0 +1,262 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + using Strings = Microsoft.VisualStudio.Text.Implementation.Strings; + + internal class ElisionSnapshot : BaseProjectionSnapshot, IProjectionSnapshot, IElisionSnapshot + { + #region State and Construction + private readonly ElisionBuffer elisionBuffer; + private readonly ITextSnapshot sourceSnapshot; + private readonly ReadOnlyCollection<ITextSnapshot> sourceSnapshots; + private readonly ElisionMap content; + private readonly bool fillInMappingMode; + + public ElisionSnapshot(ElisionBuffer elisionBuffer, + ITextSnapshot sourceSnapshot, + ITextVersion2 version, + StringRebuilder builder, + ElisionMap content, + bool fillInMappingMode) + : base(version, builder) + { + this.elisionBuffer = elisionBuffer; + this.sourceSnapshot = sourceSnapshot; + // The SourceSnapshots property is used heavily, so cache a handy copy. + this.sourceSnapshots = new ReadOnlyCollection<ITextSnapshot>(new FrugalList<ITextSnapshot>() { sourceSnapshot }); + this.totalLength = content.Length; + this.content = content; + this.totalLineCount = content.LineCount; + this.fillInMappingMode = fillInMappingMode; + Debug.Assert(this.totalLength == version.Length, + string.Format(System.Globalization.CultureInfo.CurrentCulture, + "Elision Snapshot Inconsistency. Content: {0}, Previous + delta: {1}", this.totalLength, version.Length)); + if (this.totalLength != version.Length) + { + throw new InvalidOperationException(Strings.InvalidLengthCalculation); + } + } + #endregion + + #region Buffers and Spans + public override IProjectionBufferBase TextBuffer + { + get { return this.elisionBuffer; } + } + + IElisionBuffer IElisionSnapshot.TextBuffer + { + get { return this.elisionBuffer; } + } + + protected override ITextBuffer TextBufferHelper + { + get { return this.elisionBuffer; } + } + + public override int SpanCount + { + get { return this.content.SpanCount; } + } + + public override ReadOnlyCollection<ITextSnapshot> SourceSnapshots + { + get { return this.sourceSnapshots; } + } + + public ITextSnapshot SourceSnapshot + { + get { return this.sourceSnapshot; } + } + + public SnapshotPoint MapFromSourceSnapshotToNearest(SnapshotPoint point) + { + return this.content.MapFromSourceSnapshotToNearest(this, point.Position); + } + + public override ITextSnapshot GetMatchingSnapshot(ITextBuffer textBuffer) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + return this.sourceSnapshot.TextBuffer == textBuffer ? this.sourceSnapshot : null; + } + + public override ITextSnapshot GetMatchingSnapshotInClosure(ITextBuffer textBuffer) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + + if (this.sourceSnapshot.TextBuffer == textBuffer) + { + return this.sourceSnapshot; + } + + IProjectionSnapshot2 projSnap = this.sourceSnapshot as IProjectionSnapshot2; + if (projSnap != null) + { + return projSnap.GetMatchingSnapshotInClosure(textBuffer); + } + + return null; + } + + public override ITextSnapshot GetMatchingSnapshotInClosure(Predicate<ITextBuffer> match) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + + if (match(this.sourceSnapshot.TextBuffer)) + { + return this.sourceSnapshot; + } + + IProjectionSnapshot2 projSnap = this.sourceSnapshot as IProjectionSnapshot2; + if (projSnap != null) + { + return projSnap.GetMatchingSnapshotInClosure(match); + } + + return null; + } + + public override ReadOnlyCollection<SnapshotSpan> GetSourceSpans(int startSpanIndex, int count) + { + if (startSpanIndex < 0) + { + throw new ArgumentOutOfRangeException("startSpanIndex"); + } + if (count < 0 || startSpanIndex + count > SpanCount) + { + throw new ArgumentOutOfRangeException("count"); + } + return new ReadOnlyCollection<SnapshotSpan>(this.content.GetSourceSpans(this.sourceSnapshot, startSpanIndex, count)); + } + + public override ReadOnlyCollection<SnapshotSpan> GetSourceSpans() + { + return GetSourceSpans(0, this.content.SpanCount); + } + #endregion + + #region Mapping + public override SnapshotPoint MapToSourceSnapshot(int position) + { + if (position < 0 || position > this.totalLength) + { + throw new ArgumentOutOfRangeException("position"); + } + FrugalList<SnapshotPoint> points = this.content.MapInsertionPointToSourceSnapshots(this, position); + if (points.Count == 1) + { + return points[0]; + } + else if (this.elisionBuffer.resolver == null) + { + return points[points.Count - 1]; + } + else + { + return points[this.elisionBuffer.resolver.GetTypicalInsertionPosition(new SnapshotPoint(this, position), new ReadOnlyCollection<SnapshotPoint>(points))]; + } + } + + public override SnapshotPoint MapToSourceSnapshot(int position, PositionAffinity affinity) + { + if (position < 0 || position > this.totalLength) + { + throw new ArgumentOutOfRangeException("position"); + } + if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) + { + throw new ArgumentOutOfRangeException("affinity"); + } + return this.content.MapToSourceSnapshot(this.sourceSnapshot, position, affinity); + } + + public override SnapshotPoint? MapFromSourceSnapshot(SnapshotPoint point, PositionAffinity affinity) + { + if (point.Snapshot != this.sourceSnapshot) + { + throw new ArgumentException("The point does not belong to a source snapshot of the projection snapshot"); + } + if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) + { + throw new ArgumentOutOfRangeException("affinity"); + } + return this.content.MapFromSourceSnapshot(this, point.Position); + } + + private ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshots(Span span, bool fillIn) + { + if (span.End > this.totalLength) + { + throw new ArgumentOutOfRangeException("span"); + } + FrugalList<SnapshotSpan> result = new FrugalList<SnapshotSpan>(); + if (fillIn) + { + this.content.MapToSourceSnapshotsInFillInMode(this.sourceSnapshot, span, result); + } + else + { + this.content.MapToSourceSnapshots(this, span, result); + } + return new ReadOnlyCollection<SnapshotSpan>(result); + } + + public override ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshots(Span span) + { + return MapToSourceSnapshots(span, this.fillInMappingMode); + } + + public override ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshotsForRead(Span span) + { + return MapToSourceSnapshots(span, false); + } + + public override ReadOnlyCollection<Span> MapFromSourceSnapshot(SnapshotSpan span) + { + if (span.Snapshot != this.sourceSnapshot) + { + throw new ArgumentException("The span does not belong to a source snapshot of the projection snapshot"); + } + FrugalList<Span> result = new FrugalList<Span>(); + this.content.MapFromSourceSnapshot(span, result); + return new ReadOnlyCollection<Span>(result); + } + + internal override ReadOnlyCollection<SnapshotPoint> MapInsertionPointToSourceSnapshots(int position, ITextBuffer excludedBuffer) + { + return new ReadOnlyCollection<SnapshotPoint>(this.content.MapInsertionPointToSourceSnapshots(this, position)); + } + + internal override ReadOnlyCollection<SnapshotSpan> MapReplacementSpanToSourceSnapshots(Span replacementSpan, ITextBuffer excludedBuffer) + { + // this implementation won't return zero-length spans on the edges as it + // should, but that's OK because it is not called in Beta1 (we never edit + // elision buffers directly). Third parties might do so, so we need a non-throwing + // implementation here. Zero-length spans will be added for Beta2. + return MapToSourceSnapshots(replacementSpan, false); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs b/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs new file mode 100644 index 0000000..67b747e --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/ProjectionBuffer.cs @@ -0,0 +1,1914 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + // todo: denormalizations that are performed may possibly be removed by exploiting OldPosition + // information in TextChange. Investigate this. + + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Diagnostics; + using System.Text; + + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Text.Differencing; + + using Strings = Microsoft.VisualStudio.Text.Implementation.Strings; + + internal sealed partial class ProjectionBuffer : BaseProjectionBuffer, IProjectionBuffer + { + #region ProjectionEdit Class + private class ProjectionEdit : Edit, ISubordinateTextEdit + { + private ProjectionBuffer projectionBuffer; + private bool subordinate; + + public ProjectionEdit(ProjectionBuffer projectionBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, object editTag) + : base(projectionBuffer, originSnapshot, options, reiteratedVersionNumber, editTag) + { + this.projectionBuffer = projectionBuffer; + this.subordinate = true; + } + + public ITextBuffer TextBuffer + { + get { return this.projectionBuffer; } + } + + // this is the master edit path -- initiated from outside + protected override ITextSnapshot PerformApply() + { + CheckActive(); + this.applied = true; + this.subordinate = false; + + ITextSnapshot result = this.baseBuffer.currentSnapshot; + + if (this.changes.Count > 0) + { + this.projectionBuffer.group.PerformMasterEdit(this.projectionBuffer, this, this.options, this.editTag); + + if (!this.Canceled) + { + result = this.baseBuffer.currentSnapshot; + } + } + else + { + // vacuous edit + this.baseBuffer.editInProgress = false; + } + + return result; + } + + public void PreApply() + { + // called for all non-vacuous edits + if (this.changes.Count > 0) + { + this.projectionBuffer.ComputeSourceEdits(this.changes); + } + } + + public void FinalApply() // TODO: make FinalApply return event raisers, eliminate FinishEdit() + { + // called for all non-vacuous edits + if (this.changes.Count > 0 || this.projectionBuffer.pendingContentChangedEventArgs.Count > 0) + { + this.projectionBuffer.group.CancelIndependentEdit(this.projectionBuffer); // just in case + IList<ITextEventRaiser> eventRaisers = this.projectionBuffer.InterpretSourceChanges(this.options, /*this.reiteratedVersionNumber,*/ this.editTag); + this.projectionBuffer.group.EnqueueEvents(eventRaisers, this.baseBuffer); + + // raise immediate events + foreach (var raiser in eventRaisers) + { + raiser.RaiseEvent(this.baseBuffer, true); + } + } + + this.projectionBuffer.editInProgress = false; + if (this.subordinate) + { + this.baseBuffer.group.FinishEdit(); + } + } + + public override void CancelApplication() + { + if (!this.canceled) + { + base.CancelApplication(); + this.projectionBuffer.editApplicationInProgress = false; + this.projectionBuffer.pendingContentChangedEventArgs.Clear(); + } + } + } + #endregion + + #region SourceBufferSet Class + /// <summary> + /// Tracks the Source TextBuffers of this ProjectionBuffer. There is exactly one SourceBufferSet + /// per ProjectionBufferImpl. + /// </summary> + private class SourceBufferSet + { + private class BufferTracker + { + public ITextBuffer _buffer; + public int _spanCount; + public BufferTracker(ITextBuffer buffer) + { + _buffer = buffer; + _spanCount = 1; + } + } + + // presumption: the number of source buffers is small + private bool _inTransaction; + private List<BufferTracker> _sourceBufferTrackers = new List<BufferTracker>(); + private FrugalList<ITextBuffer> _addedBuffers; + private FrugalList<ITextBuffer> _removedBuffers; + + private int Find(ITextBuffer buffer) + { + for (int i = 0; i < _sourceBufferTrackers.Count; ++i) + { + if (buffer == _sourceBufferTrackers[i]._buffer) + { + return i; + } + } + return -1; + } + + public List<ITextBuffer> SourceBuffers + { + get + { + List<ITextBuffer> sourceBuffers = new List<ITextBuffer>(); + foreach (BufferTracker bt in _sourceBufferTrackers) + { + if (!sourceBuffers.Contains(bt._buffer)) + { + sourceBuffers.Add(bt._buffer); + } + } + return sourceBuffers; + } + } + + public void StartTransaction() + { + Debug.Assert(!_inTransaction); + _addedBuffers = new FrugalList<ITextBuffer>(); + _removedBuffers = new FrugalList<ITextBuffer>(); + _inTransaction = true; + } + + public void FinishTransaction(out FrugalList<ITextBuffer> addedBuffers, out FrugalList<ITextBuffer> removedBuffers) + { + Debug.Assert(_inTransaction); + + // If a buffer was removed and then added, eliminate it from both lists. + // Since these lists should be extremely short, nothing fancy here. + FrugalList<ITextBuffer> comingAndGoingBuffers = new FrugalList<ITextBuffer>(); + foreach (ITextBuffer buffer in _addedBuffers) + { + if (_removedBuffers.Contains(buffer)) + { + comingAndGoingBuffers.Add(buffer); + } + } + foreach (ITextBuffer buffer in comingAndGoingBuffers) + { + _addedBuffers.Remove(buffer); + _removedBuffers.Remove(buffer); + } + + addedBuffers = _addedBuffers; + removedBuffers = _removedBuffers; + _addedBuffers = null; + _removedBuffers = null; + _inTransaction = false; + } + + public void AddSpan(ITrackingSpan span) + { + Debug.Assert(_inTransaction); + int i = Find(span.TextBuffer); + if (i < 0) + { + _sourceBufferTrackers.Add(new BufferTracker(span.TextBuffer)); + _addedBuffers.Add(span.TextBuffer); + } + else + { + _sourceBufferTrackers[i]._spanCount++; + } + } + + public void RemoveSpan(ITrackingSpan span) + { + Debug.Assert(_inTransaction); + int i = Find(span.TextBuffer); + Debug.Assert(i >= 0); + if (--_sourceBufferTrackers[i]._spanCount == 0) + { + _sourceBufferTrackers.RemoveAt(i); + _removedBuffers.Add(span.TextBuffer); + } + } + } + + public override IList<ITextBuffer> SourceBuffers + { + // this is problematic, but we need it until the buffer graph implementation catches up to the new world + get + { + return this.sourceBufferSet.SourceBuffers; + } + } + #endregion + + #region Private State + private ProjectionBufferOptions bufferOptions; + private IDifferenceService differenceService; + private List<ITrackingSpan> sourceSpans = new List<ITrackingSpan>(); + private SourceBufferSet sourceBufferSet = new SourceBufferSet(); + private ProjectionSnapshot currentProjectionSnapshot; + + private IInternalTextBufferFactory textBufferFactory; + internal ITextBuffer literalBuffer; + private IReadOnlyRegion literalBufferRor; + + private List<WeakEventHook> eventHooks = new List<WeakEventHook>(); + #endregion + + #region Construction + public ProjectionBuffer(IInternalTextBufferFactory textBufferFactory, + IProjectionEditResolver resolver, + IContentType contentType, + IList<object> initialSourceSpans, + IDifferenceService differenceService, + ITextDifferencingService textDifferencingService, + ProjectionBufferOptions options, + GuardedOperations guardedOperations) + : base(resolver, contentType, textDifferencingService, guardedOperations) + { + // Parameters are validated outside + Debug.Assert(initialSourceSpans != null); + this.textBufferFactory = textBufferFactory; + this.differenceService = differenceService; + this.bufferOptions = options; + + SpanManager spanManager = new SpanManager(this, 0, 0, initialSourceSpans, false, false); + spanManager.PerformChecks(); + spanManager.ProcessLiteralSpans(); + + int spanPos = 0; + this.sourceBufferSet.StartTransaction(); + int initialLength = 0; + List<SnapshotSpan> snapshotSpans = new List<SnapshotSpan>(); + foreach (ITrackingSpan initialTrackingSpan in spanManager.SpansToInsert) + { + AddSpan(spanPos++, initialTrackingSpan); + SnapshotSpan snapSpan = new SnapshotSpan(initialTrackingSpan.TextBuffer.CurrentSnapshot, + initialTrackingSpan.GetSpan(initialTrackingSpan.TextBuffer.CurrentSnapshot)); + initialLength += snapSpan.Length; + snapshotSpans.Add(snapSpan); + } + + StringRebuilder newBuilder = StringRebuilder.Empty; + for (int i = 0; (i < snapshotSpans.Count); ++i) + newBuilder = newBuilder.Append(BufferFactoryService.StringRebuilderFromSnapshotSpan(snapshotSpans[i])); + this.builder = newBuilder; + + this.currentVersion.SetLength(initialLength); // this is a bit hacky + + FrugalList<ITextBuffer> addedBuffers; + FrugalList<ITextBuffer> removedBuffers; + this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers); + + // listen to changes to source buffers + bool firstAddedBuffer = true; + BufferGroup chosenGroup = null; + foreach (ITextBuffer addedBuffer in addedBuffers) + { + BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer; + if (firstAddedBuffer) + { + firstAddedBuffer = false; + chosenGroup = baseAddedBuffer.group; + chosenGroup.AddMember(this); + } + else + { + chosenGroup.Swallow(baseAddedBuffer.group); + } + if ((baseAddedBuffer != this.literalBuffer) || ((this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0)) + { + this.eventHooks.Add(new WeakEventHook(this, baseAddedBuffer)); + } + } + this.group = chosenGroup ?? new BufferGroup(this); + + this.currentProjectionSnapshot = new ProjectionSnapshot(this, base.currentVersion, this.builder, snapshotSpans); + this.currentSnapshot = this.currentProjectionSnapshot; + } + #endregion + + #region Span Editing and Management + private class SourceSpansChangedEventRaiser : ITextEventRaiser + { + ProjectionSourceSpansChangedEventArgs args; + + public SourceSpansChangedEventRaiser(ProjectionSourceSpansChangedEventArgs args) + { + this.args = args; + } + + public void RaiseEvent(BaseBuffer baseBuffer, bool immediate) + { + ProjectionBuffer projBuffer = (ProjectionBuffer)baseBuffer; + ProjectionSourceBuffersChangedEventArgs bufferArgs = args as ProjectionSourceBuffersChangedEventArgs; + if (bufferArgs != null) + { + EventHandler<ProjectionSourceBuffersChangedEventArgs> bufferHandlers = + immediate ? projBuffer.SourceBuffersChangedImmediate : projBuffer.SourceBuffersChanged; + if (bufferHandlers != null) + { + bufferHandlers(baseBuffer, bufferArgs); + } + } + + EventHandler<ProjectionSourceSpansChangedEventArgs> spanHandlers = + immediate ? projBuffer.SourceSpansChangedImmediate : projBuffer.SourceSpansChanged; + if (spanHandlers != null) + { + spanHandlers(baseBuffer, args); + } + + // now raise the text content changed event + baseBuffer.RawRaiseEvent(args, immediate); + } + + public bool HasPostEvent + { + get { return true; } + } + } + + /// <summary> + /// Perform validity checking and normalization of source spans that are to be inserted. Checks + /// for spans that are null, of the wrong type (neither string nor ITrackingSpan), would induce + /// projection buffer cycles, have the wrong tracking mode, or induce projection overlaps. + /// Also converts string literals to ITrackingSpans over the literal source buffer. + /// </summary> + private class SpanManager + { + public int Position { get; private set; } + public int SpansToDelete { get; private set; } + public List<object> RawSpansToInsert { get; private set; } + + // The set of text buffers that have been previously visited in a cyclic dependency check + private Dictionary<ITextBuffer, bool> visitedBufferSet = new Dictionary<ITextBuffer, bool>(); + private ProjectionBuffer projBuffer; + private List<ITrackingSpan> spansToInsert; + private LiteralBufferHelper lit; + private bool checkForCycles; + + public SpanManager(ProjectionBuffer projBuffer, int position, int spansToDelete, IList<object> rawSpansToInsert, bool checkForCycles, bool groupEdit) + { + this.projBuffer = projBuffer; + this.checkForCycles = checkForCycles; + this.lit = new LiteralBufferHelper(projBuffer, (this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0, groupEdit); + + this.Position = position; + this.SpansToDelete = spansToDelete; + this.RawSpansToInsert = new List<object>(rawSpansToInsert); + } + + public List<ITrackingSpan> SpansToInsert + { + get + { + if (this.spansToInsert == null) + { + this.lit.FinishEdit(); + this.spansToInsert = new List<ITrackingSpan>(); + foreach (object rawSpan in this.RawSpansToInsert) + { + ITrackingSpan trackingSpan = rawSpan as ITrackingSpan; + if (trackingSpan == null) + { + trackingSpan = this.projBuffer.literalBuffer.CurrentSnapshot.CreateTrackingSpan((Span)rawSpan, SpanTrackingMode.EdgeExclusive, TrackingFidelityMode.Forward); + } + this.spansToInsert.Add(trackingSpan); + } + } + return this.spansToInsert; + } + } + + private void CheckForSourceBufferCycle(ITextBuffer buffer) + { + // TODO shouldn't we load up the visitedbufferset with existing source buffers first? at least for perf. + if (!this.visitedBufferSet.ContainsKey(buffer)) + { + if (buffer == this.projBuffer) + { + throw new ArgumentException(Strings.SourceBufferCycle); + } + this.visitedBufferSet.Add(buffer, true); + IProjectionBuffer p = buffer as ProjectionBuffer; + if (p != null) + { + foreach (ITextBuffer sourceBuffer in p.SourceBuffers) + { + CheckForSourceBufferCycle(sourceBuffer); + } + } + } + } + + private static void CheckTrackingMode(ITrackingSpan spanToInsert) + { + if (spanToInsert.TrackingMode != SpanTrackingMode.EdgeExclusive && spanToInsert.TrackingMode != SpanTrackingMode.Custom) + { + ITextSnapshot snap = spanToInsert.TextBuffer.CurrentSnapshot; + Span span = spanToInsert.GetSpan(snap); + if (spanToInsert.TrackingMode == SpanTrackingMode.EdgeInclusive) + { + if (span.Start > 0 || span.End < snap.Length) + { + throw new ArgumentException(Strings.InvalidEdgeInclusiveSourceSpan); + } + } + else if (spanToInsert.TrackingMode == SpanTrackingMode.EdgePositive) + { + if (span.End < snap.Length) + { + throw new ArgumentException(Strings.InvalidEdgePositiveSourceSpan); + } + } + else if (span.Start > 0) + { + throw new ArgumentException(Strings.InvalidEdgeNegativeSourceSpan); + } + } + } + + private IEnumerable<ITrackingSpan> GetProposedSpans() + { + for (int s = 0; s < this.Position; ++s) + { + yield return projBuffer.sourceSpans[s]; + } + for (int s = this.Position + this.SpansToDelete; s < projBuffer.sourceSpans.Count; ++s) + { + yield return projBuffer.sourceSpans[s]; + } + for (int s = 0; s < this.RawSpansToInsert.Count; ++s) + { + ITrackingSpan ts = this.RawSpansToInsert[s] as ITrackingSpan; + if (ts != null) + { + yield return ts; + } + } + } + + /// <summary> + /// Build a list of ultimate sources for the given span, looking through all projections + /// </summary> + /// <param name="span"></param> + /// <returns></returns> + private IList<SnapshotSpan> BaseSourceSpans(SnapshotSpan span) + { + List<SnapshotSpan> result = new List<SnapshotSpan>(); + if (span.Snapshot.TextBuffer is IProjectionBuffer) + { + foreach (SnapshotSpan s in ((IProjectionSnapshot)span.Snapshot).MapToSourceSnapshots(span)) + { + result.AddRange(BaseSourceSpans(s)); + } + } + else + { + result.Add(span); + } + return result; + } + + private void CheckOverlap() + { + Dictionary<ITextBuffer, List<Span>> bufferToSpans = new Dictionary<ITextBuffer, List<Span>>(); + foreach (ITrackingSpan proposedSpan in GetProposedSpans()) + { + // Look through all projections + IList<SnapshotSpan> baseSpans = BaseSourceSpans(proposedSpan.GetSpan(proposedSpan.TextBuffer.CurrentSnapshot)); + + // Group by source buffer + foreach (SnapshotSpan baseSpan in baseSpans) + { + List<Span> spans; + ITextBuffer buffer = baseSpan.Snapshot.TextBuffer; + if (!bufferToSpans.TryGetValue(buffer, out spans)) + { + spans = new List<Span>(); + bufferToSpans.Add(buffer, spans); + } + spans.Add(baseSpan); + } + } + + foreach (KeyValuePair<ITextBuffer, List<Span>> bufferSpans in bufferToSpans) + { + if (bufferSpans.Value.Count > 1) + { + // sort and check for overlap. + // sort by start position, except if two spans start at the same position sort by + // end position so that a null span comes first + bufferSpans.Value.Sort(delegate(Span x, Span y) { return x.Start == y.Start ? x.End - y.End : x.Start - y.Start; }); + for (int s = 1; s < bufferSpans.Value.Count; ++s) + { + if (bufferSpans.Value[s].Start < bufferSpans.Value[s - 1].End) + { + throw new ArgumentException(Strings.OverlappingSourceSpans); + } + } + } + } + } + + public void PerformChecks() + { + foreach (object spanToInsert in this.RawSpansToInsert) + { + if (spanToInsert == null) + { + throw new ArgumentNullException("spansToInsert"); + } + ITrackingSpan trackingSpanToInsert = spanToInsert as ITrackingSpan; + if (trackingSpanToInsert != null) + { + if (checkForCycles) + { + try + { + CheckForSourceBufferCycle(trackingSpanToInsert.TextBuffer); + } + catch (ArgumentException) + { + throw new ArgumentException(Strings.SourceBufferCycle); + } + } + if ((this.projBuffer.bufferOptions & ProjectionBufferOptions.PermissiveEdgeInclusiveSourceSpans) == 0) + { + CheckTrackingMode(trackingSpanToInsert); + } + } + else + { + if (!(spanToInsert is string)) + { + throw new ArgumentException(Strings.NeitherSpanNorString); + } + } + } + CheckOverlap(); + } + + public void ProcessLiteralSpans() + { + // must do deletions first! + for (int d = this.Position; d < this.Position + this.SpansToDelete; ++d) + { + ITrackingSpan sourceSpan = this.projBuffer.sourceSpans[d]; + if (sourceSpan.TextBuffer == this.projBuffer.literalBuffer) + { + this.lit.Delete(sourceSpan); + } + } + + for (int r = 0; r < this.RawSpansToInsert.Count; ++r) + { + object rawSpan = this.RawSpansToInsert[r]; + string literalSpan = rawSpan as string; + if (literalSpan != null) + { + // change the string into a Span indicating what the bounds of + // the span will be when it is ready + this.RawSpansToInsert[r] = this.lit.Append(literalSpan); + } + } + } + } + + private class LiteralBufferHelper + { + private ProjectionBuffer projBuffer; + bool performedEdit; + bool readOnly; + bool groupEdit; + int totalInsertions; + int totalDeletions; + int insertionPoint; + + public LiteralBufferHelper(ProjectionBuffer projBuffer, bool readOnly, bool groupEdit) + { + this.projBuffer = projBuffer; + this.readOnly = readOnly; + this.groupEdit = groupEdit; + if (this.projBuffer.literalBuffer != null) + { + this.insertionPoint = this.projBuffer.literalBuffer.CurrentSnapshot.Length; + } + } + + private void PrepareEdit() + { + if (this.projBuffer.literalBuffer == null) + { + this.projBuffer.literalBuffer = + projBuffer.textBufferFactory.CreateTextBuffer(string.Empty, projBuffer.textBufferFactory.InertContentType, readOnly); + this.insertionPoint = 0; + } + else if (this.projBuffer.literalBufferRor != null) + { + using (IReadOnlyRegionEdit rorEdit = this.projBuffer.literalBuffer.CreateReadOnlyRegionEdit()) + { + rorEdit.RemoveReadOnlyRegion(this.projBuffer.literalBufferRor); + rorEdit.Apply(); + } + this.projBuffer.literalBufferRor = null; + } + this.performedEdit = true; + } + + public Span Append(string text) + { + PrepareEdit(); + Span result; + if (this.groupEdit) + { + ITextEdit edit = this.projBuffer.group.GetEdit((BaseBuffer)this.projBuffer.literalBuffer, EditOptions.None); + edit.Insert(this.insertionPoint, text); + result = new Span(this.insertionPoint + this.totalInsertions - this.totalDeletions, text.Length); + this.totalInsertions += text.Length; + } + else + { + ITextSnapshot literalSnap = projBuffer.literalBuffer.CurrentSnapshot; + int length = literalSnap.Length; + literalSnap = projBuffer.literalBuffer.Insert(length, text); + result = new Span(length, text.Length); + } + return result; + } + + public void Delete(ITrackingSpan trackingSpan) + { + PrepareEdit(); + Span span = trackingSpan.GetSpan(projBuffer.literalBuffer.CurrentSnapshot); + if (this.groupEdit) + { + ITextEdit edit = this.projBuffer.group.GetEdit((BaseBuffer)this.projBuffer.literalBuffer, EditOptions.None); + edit.Delete(span); + totalDeletions += span.Length; + } + else + { + projBuffer.literalBuffer.Delete(span); + } + } + + public void FinishEdit() + { + if (this.performedEdit && this.readOnly) + { + if (this.projBuffer.literalBuffer != null) + { + Debug.Assert(this.projBuffer.literalBufferRor == null); + using (IReadOnlyRegionEdit rorEdit = this.projBuffer.literalBuffer.CreateReadOnlyRegionEdit()) + { + this.projBuffer.literalBufferRor = + rorEdit.CreateReadOnlyRegion(new Span(0, rorEdit.Snapshot.Length), SpanTrackingMode.EdgeInclusive, EdgeInsertionMode.Deny); + rorEdit.Apply(); + } + } + } + } + } + + private class SpanEdit : TextBufferBaseEdit, ISubordinateTextEdit + { + private ProjectionBuffer projBuffer; + private EditOptions editOptions = EditOptions.None; + private object tag = null; + private SpanManager spanManager; + + public SpanEdit(ProjectionBuffer projBuffer) : base(projBuffer) + { + this.projBuffer = projBuffer; + } + + public ITextBuffer TextBuffer + { + get { return this.projBuffer; } + } + + public IProjectionSnapshot ReplaceSpans(int position, int spansToReplace, IList<object> spansToInsert, EditOptions options, object editTag) + { + if (position < 0 || position > this.projBuffer.sourceSpans.Count) + { + throw new ArgumentOutOfRangeException("position"); + } + if (spansToReplace < 0 || position + spansToReplace > this.projBuffer.sourceSpans.Count) + { + throw new ArgumentOutOfRangeException("spansToReplace"); + } + if (spansToInsert == null) + { + throw new ArgumentNullException("spansToInsert"); + } + + this.spanManager = new SpanManager(this.projBuffer, position, spansToReplace, spansToInsert, true, (this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0); + this.editOptions = options; + this.tag = editTag; + + this.spanManager.PerformChecks(); + + // we are committed! + + this.applied = true; + if (this.spanManager.SpansToDelete > 0 || this.spanManager.RawSpansToInsert.Count > 0) + { + // non-vacuous + this.projBuffer.group.PerformMasterEdit(this.projBuffer, this, this.editOptions, this.tag); + } + this.baseBuffer.group.FinishEdit(); + this.baseBuffer.editInProgress = false; + return this.projBuffer.currentProjectionSnapshot; + } + + public void PreApply() + { + this.projBuffer.editApplicationInProgress = true; + this.spanManager.ProcessLiteralSpans(); + } + + public void FinalApply() + { + ProjectionSourceSpansChangedEventArgs args = this.projBuffer.ApplySpanChanges(this.spanManager.Position, this.spanManager.SpansToDelete, this.spanManager.SpansToInsert, this.editOptions, this.tag); + SourceSpansChangedEventRaiser raiser = new SourceSpansChangedEventRaiser(args); + this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer); + raiser.RaiseEvent(this.baseBuffer, true); + this.baseBuffer.editInProgress = false; + this.projBuffer.editApplicationInProgress = false; + + if ((this.projBuffer.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0) + { + // the only pending changes should be changes to our literal buffer + Debug.Assert(this.projBuffer.pendingContentChangedEventArgs.Count <= 1); + if (this.projBuffer.pendingContentChangedEventArgs.Count == 1) + { + Debug.Assert(this.projBuffer.pendingContentChangedEventArgs[0].Before.TextBuffer == this.projBuffer.literalBuffer); + } + // forget about changes to our literal buffer; we've already incorporated them + this.projBuffer.pendingContentChangedEventArgs.Clear(); + } + else + { + Debug.Assert(this.projBuffer.pendingContentChangedEventArgs.Count == 0); + } + } + + public bool CheckForCancellation(Action cancelAction) + { + return true; + } + + public override string ToString() + { + StringBuilder insertions = new StringBuilder(); + for (int t = 0; t < this.spanManager.RawSpansToInsert.Count; ++t) + { + ITrackingSpan insertion = this.spanManager.RawSpansToInsert[t] as ITrackingSpan; + if (insertion != null) + { + insertions.Append(insertion.ToString()); + } + else + { + insertions.Append("(Literal)"); + } + if (t < this.spanManager.RawSpansToInsert.Count - 1) + { + insertions.Append(","); + } + } + return string.Format(System.Globalization.CultureInfo.CurrentCulture, + "pos: {0}, delete: {1}, insert: {2}", + this.spanManager.Position, this.spanManager.SpansToDelete, insertions.ToString()); + } + + public void RecordMasterChangeOffset(int masterChangeOffset) + { + throw new InvalidOperationException("Projection span edits shouldn't have change offsets."); + } + } + + private ProjectionSourceSpansChangedEventArgs ApplySpanChanges(int position, int spansToDelete, IList<ITrackingSpan> spansToInsert, EditOptions options, object editTag) + { + ProjectionSnapshot beforeSnapshot = this.currentProjectionSnapshot; + + List<ITrackingSpan> deletedSpans = new List<ITrackingSpan>(); + + List<SnapshotSpan> insertedSnapSpans = new List<SnapshotSpan>(); + List<SnapshotSpan> deletedSnapSpans = new List<SnapshotSpan>(); + + this.sourceBufferSet.StartTransaction(); + + for (int i = position + spansToDelete - 1; i >= position; --i) + { + ITrackingSpan removedSpan = RemoveSpan(i); + deletedSpans.Insert(0, removedSpan); // preserve order! + deletedSnapSpans.Insert(0, this.currentProjectionSnapshot.GetSourceSpan(i)); + } + + int insertPosition = position; + foreach (ITrackingSpan span in spansToInsert) + { + AddSpan(insertPosition++, span); + insertedSnapSpans.Add(span.GetSpan(span.TextBuffer.CurrentSnapshot)); + } + + FrugalList<ITextBuffer> addedBuffers; + FrugalList<ITextBuffer> removedBuffers; + this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers); + + // todo make this transactional in case it fails here, or else check thread affinity earlier + // todo combine with later loop (can it move up here?) + foreach (ITextBuffer addedBuffer in addedBuffers) + { + BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer; + this.group.Swallow(baseAddedBuffer.group); + } + + int textPosition = 0; + for (int i = 0; i < position; ++i) + { + textPosition += this.currentProjectionSnapshot.GetSourceSpan(i).Length; + } + + INormalizedTextChangeCollection normalizedChanges; + if (options.ComputeMinimalChange) + { + normalizedChanges = ComputeTextChangesByStringDiffing(options.DifferenceOptions, textPosition, deletedSnapSpans, insertedSnapSpans); + } + else + { + normalizedChanges = ComputeTextChangesBySpanDiffing(textPosition, deletedSnapSpans, insertedSnapSpans); + } + + SetCurrentVersionAndSnapshot(normalizedChanges); + + ProjectionSourceSpansChangedEventArgs args = null; + if (addedBuffers.Count > 0 || removedBuffers.Count > 0) + { + // Adjust buffer change listening + foreach (ITextBuffer addedBuffer in addedBuffers) + { + if (addedBuffer != this.literalBuffer || (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0) + { + BaseBuffer baseAddedBuffer = (BaseBuffer)addedBuffer; + this.eventHooks.Add(new WeakEventHook(this, baseAddedBuffer)); + } + } + foreach (ITextBuffer removedBuffer in removedBuffers) + { + if (removedBuffer != this.literalBuffer || (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) != 0) + { + BaseBuffer baseRemovedBuffer = (BaseBuffer)removedBuffer; + + for (int i = 0; (i < this.eventHooks.Count); ++i) + { + var hook = this.eventHooks[i]; + if (hook.SourceBuffer == baseRemovedBuffer) + { + hook.UnsubscribeFromSourceBuffer(); + this.eventHooks.RemoveAt(i); + break; + } + } + } + } + + args = new ProjectionSourceBuffersChangedEventArgs + (beforeSnapshot, this.currentProjectionSnapshot, + spansToInsert, deletedSpans, position, addedBuffers, removedBuffers, options, editTag); + } + else + { + args = new ProjectionSourceSpansChangedEventArgs + (beforeSnapshot, this.currentProjectionSnapshot, + spansToInsert, deletedSpans, position, options, editTag); + } + return args; + } + + private INormalizedTextChangeCollection ComputeTextChangesByStringDiffing(StringDifferenceOptions differenceOptions, int textPosition, List<SnapshotSpan> deletedSnapSpans, List<SnapshotSpan> insertedSnapSpans) + { + StringBuilder oldText = new StringBuilder(); + foreach (SnapshotSpan deletedSnapSpan in deletedSnapSpans) + { + oldText.Append(deletedSnapSpan.GetText()); + } + + StringBuilder newText = new StringBuilder(); + foreach (SnapshotSpan insertedSnapSpan in insertedSnapSpans) + { + newText.Append(insertedSnapSpan.GetText()); + } + + List<TextChange> changes = new List<TextChange>(); + if (oldText.Length > 0 || newText.Length > 0) + { + changes.Add(TextChange.Create(textPosition, oldText.ToString(), newText.ToString(), this.currentProjectionSnapshot)); + } + + return NormalizedTextChangeCollection.Create(changes, differenceOptions, this.textDifferencingService); + } + + private INormalizedTextChangeCollection ComputeTextChangesBySpanDiffing(int textPosition, List<SnapshotSpan> deletedSnapSpans, List<SnapshotSpan> insertedSnapSpans) + { + ProjectionSpanDiffer differ = new ProjectionSpanDiffer(this.differenceService, deletedSnapSpans.AsReadOnly(), insertedSnapSpans.AsReadOnly()); + return new ProjectionSpanToNormalizedChangeConverter(differ, textPosition, this.currentProjectionSnapshot).NormalizedChanges; + } + + private void AddSpan(int position, ITrackingSpan sourceSpan) + { + this.sourceSpans.Insert(position, sourceSpan); + this.sourceBufferSet.AddSpan(sourceSpan); + } + + private ITrackingSpan RemoveSpan(int position) + { + ITrackingSpan result = this.sourceSpans[position]; + this.sourceSpans.RemoveAt(position); + this.sourceBufferSet.RemoveSpan(result); + return result; + } + + public IProjectionSnapshot InsertSpan(int position, ITrackingSpan spanToInsert) + { + return ReplaceSpans(position, 0, new List<object>(1) { spanToInsert }, EditOptions.None, null); + } + + public IProjectionSnapshot InsertSpan(int position, string literalSpanToInsert) + { + return ReplaceSpans(position, 0, new List<object>(1) { literalSpanToInsert }, EditOptions.None, null); + } + + public IProjectionSnapshot InsertSpans(int position, IList<object> spansToInsert) + { + return ReplaceSpans(position, 0, spansToInsert, EditOptions.None, null); + } + + public IProjectionSnapshot DeleteSpans(int position, int spansToDelete) + { + return ReplaceSpans(position, spansToDelete, new List<object>(0), EditOptions.None, null); + } + + public IProjectionSnapshot ReplaceSpans(int position, int spansToReplace, IList<object> spansToInsert, EditOptions options, object editTag) + { + using (SpanEdit spedit = new SpanEdit(this)) + { + return spedit.ReplaceSpans(position, spansToReplace, spansToInsert, options, editTag); + } + } + #endregion + + #region Edit Creation + public override ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) + { + return new ProjectionEdit(this, this.currentProjectionSnapshot, options, reiteratedVersionNumber, editTag); + } + + protected internal override ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) + { + return new ProjectionEdit(this, this.currentProjectionSnapshot, options, reiteratedVersionNumber, editTag); + } + #endregion + + #region Event Handling + /// <summary> + /// Respond to a content change in a source buffer by mapping the change events to the coordinate system of + /// this projection buffer. + /// </summary> + internal override void OnSourceTextChanged(object sender, TextContentChangedEventArgs e) + { + this.pendingContentChangedEventArgs.Add(e); + + if (!this.editApplicationInProgress) + { + // We had better be a member of the same group as the buffer that we just heard from + Debug.Assert(this.group.MembersContains(e.After.TextBuffer)); + // Let the buffer group decide when to issue events; this allows us to collect changes from multiple + // source buffers into a single snapshot, and (more important) to avoid inconsistencies + this.group.ScheduleIndependentEdit(this); + } + } + #endregion + + #region Source Change Interpretation + + public override BaseBuffer.ITextEventRaiser PropagateSourceChanges(EditOptions options, object editTag) + { + IList<ITextEventRaiser> eventRaisers = InterpretSourceChanges(options, /*this.reiteratedVersionNumber,*/ editTag); + Debug.Assert(eventRaisers.Count == 1); + eventRaisers[0].RaiseEvent(this, true); + return eventRaisers[0]; + } + + /// <summary> + /// TextChanges that are to be accomplished by a change to source spans (because of EdgeExclusive anomalies). + /// </summary> + private class SpanAdjustment + { + public TextChange LeadingChange; // Text change induced by span growth/shrinkage on the leading edge + public TextChange TrailingChange; // Text change induced by span growth/shrinkage on the trailing edge + } + + private IList<ITextEventRaiser> InterpretSourceChanges(EditOptions options, object editTag) + { + // now that all source edits have been applied, we can interpret the events they raised + List<ITextEventRaiser> eventRaisers = new List<ITextEventRaiser>(); + + SortedDictionary<int, SpanAdjustment> spanPreAdjustments = new SortedDictionary<int, SpanAdjustment>(); + SortedDictionary<int, SpanAdjustment> spanPostAdjustments = new SortedDictionary<int, SpanAdjustment>(); + + INormalizedTextChangeCollection normalizedChanges = ComputeProjectedChanges(spanPreAdjustments, spanPostAdjustments); + + // Shrink source spans for each span pre-adjustment. Each one generates a new projection version/snapshot. + if (BufferGroup.Tracing) + { + Debug.WriteLine("Span Preadjustments"); + } + foreach (KeyValuePair<int, SpanAdjustment> positionAndAdjustment in spanPreAdjustments) + { + eventRaisers.Add(PerformSpanAdjustments(positionAndAdjustment.Key, positionAndAdjustment.Value, true, editTag)); + } + + if (BufferGroup.Tracing) + { + Debug.WriteLine("Main Snapshot"); + } + //if (normalizedChanges.Count > 0) // TODO: optimize this snapshot away if Count == 0 + { + ITextSnapshot originSnapshot = this.currentProjectionSnapshot; + base.SetCurrentVersionAndSnapshot(normalizedChanges); + eventRaisers.Add(new TextContentChangedEventRaiser(originSnapshot, this.currentProjectionSnapshot, options, editTag)); + } + + // Generate additional transaction(s) that alter EdgeExclusive source spans as needed + if (BufferGroup.Tracing) + { + Debug.WriteLine("Span Postadjustments"); + } + foreach (KeyValuePair<int, SpanAdjustment> positionAndAdjustment in spanPostAdjustments) + { + eventRaisers.Add(PerformSpanAdjustments(positionAndAdjustment.Key, positionAndAdjustment.Value, false, editTag)); + } + + this.editApplicationInProgress = false; + return eventRaisers; + } + + private SourceSpansChangedEventRaiser PerformSpanAdjustments(int spanPosition, SpanAdjustment adjustment, bool beforeBaseSnapshot, object editTag) + { + IProjectionSnapshot originSnapshot = this.currentProjectionSnapshot; + List<SnapshotSpan> newSourceSpans = new List<SnapshotSpan>(originSnapshot.GetSourceSpans()); + this.sourceBufferSet.StartTransaction(); + + ITrackingSpan deletedSpan = RemoveSpan(spanPosition); + SnapshotSpan sourceOriginSpan = originSnapshot.GetSourceSpans(spanPosition, 1)[0]; + int start = sourceOriginSpan.Start; + int end = sourceOriginSpan.End; + + List<TextChange> changes = new List<TextChange>(); + if (adjustment.LeadingChange != null) + { + changes.Add(adjustment.LeadingChange); + if (beforeBaseSnapshot) + { + start += adjustment.LeadingChange.OldLength; + } + else + { + start -= adjustment.LeadingChange.NewLength; + } + } + if (adjustment.TrailingChange != null) + { + changes.Add(adjustment.TrailingChange); + if (beforeBaseSnapshot) + { + end -= adjustment.TrailingChange.OldLength; + } + else + { + end += adjustment.TrailingChange.NewLength; + } + } + ITrackingSpan insertedSpan = sourceOriginSpan.Snapshot.CreateTrackingSpan(Span.FromBounds(start, end), deletedSpan.TrackingMode); + AddSpan(spanPosition, insertedSpan); + newSourceSpans[spanPosition] = new SnapshotSpan(sourceOriginSpan.Snapshot, Span.FromBounds(start, end)); + + FrugalList<ITextBuffer> addedBuffers; + FrugalList<ITextBuffer> removedBuffers; + this.sourceBufferSet.FinishTransaction(out addedBuffers, out removedBuffers); + + INormalizedTextChangeCollection normalizedChanges = NormalizedTextChangeCollection.Create(changes); + + this.currentVersion = this.currentVersion.CreateNext(normalizedChanges, newLength: -1, reiteratedVersionNumber: -1); + this.builder = this.ApplyChangesToStringRebuilder(normalizedChanges, this.builder); + + if (beforeBaseSnapshot) + { + this.currentProjectionSnapshot = TakeStaticSnapshot(newSourceSpans); + } + else + { + this.currentSnapshot = TakeSnapshot(); + } + + ProjectionSourceSpansChangedEventArgs args = + new ProjectionSourceSpansChangedEventArgs(originSnapshot, this.currentProjectionSnapshot, + new ITrackingSpan[] { insertedSpan }, new ITrackingSpan[] { deletedSpan }, + spanPosition, EditOptions.None, editTag); + return new SourceSpansChangedEventRaiser(args); + } + + private void DeleteFromSource(SnapshotSpan deletionSpan) + { + ITextBuffer sourceBuffer = deletionSpan.Snapshot.TextBuffer; + ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer); + xedit.Delete(deletionSpan); + } + + private void ReplaceInSource(SnapshotSpan deletionSpan, string insertionText, int masterChangeOffset) + { + ITextBuffer sourceBuffer = deletionSpan.Snapshot.TextBuffer; + ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer); + xedit.Replace(deletionSpan, insertionText); + + ((ISubordinateTextEdit)xedit).RecordMasterChangeOffset(masterChangeOffset); + // This above cast is safe because the buffer group stores edits of type ISubordinateTextEdit + // and casts them to ITextEdit before returning them. + } + + private void InsertInSource(SnapshotPoint sourceInsertionPoint, string text, int masterChangeOffset) + { + ITextBuffer sourceBuffer = sourceInsertionPoint.Snapshot.TextBuffer; + ITextEdit xedit = this.group.GetEdit((BaseBuffer)sourceBuffer); + xedit.Insert(sourceInsertionPoint.Position, text); + + ((ISubordinateTextEdit)xedit).RecordMasterChangeOffset(masterChangeOffset); + // This above cast is safe because the buffer group stores edits of type ISubordinateTextEdit + // and casts them to ITextEdit before returning them. + } + + /// <summary> + /// Generate the set of normalized text changes to the projection buffer that are induced by the current set of + /// pending source changes. + /// </summary> + /// <param name="spanPreAdjustments">Adjustments to be manifested as shrinkage of EdgeExclusive source spans before adopting new source snapshot(s).</param> + /// <param name="spanPostAdjustments">Adjustments to be manifested as growth of EdgeExclusive source spans adopting new source snapshot(s).</param> + /// <returns></returns> + private INormalizedTextChangeCollection ComputeProjectedChanges(SortedDictionary<int, SpanAdjustment> spanPreAdjustments, + SortedDictionary<int, SpanAdjustment> spanPostAdjustments) + { + DumpPendingContentChangedEventArgs(); + List<Tuple<ITextBuffer, List<TextChange>>> pendingSourceChanges = PreparePendingChanges(); + List<TextChange> projectedChanges = new List<TextChange>(); + + DumpPendingChanges(pendingSourceChanges); + + // these are the points in ordinary text buffers at which we have performed span adjustments + // to account for edge-exclusive anomalies. We track them so that we never repeat the adjustment + // for the same change. + HashSet<SnapshotPoint> urPoints = new HashSet<SnapshotPoint>(); + + // Process the pending changes in the reverse of the order in which we received them. This means + // that the 'nearest' buffers are processed first. + for (int p = pendingSourceChanges.Count - 1; p >= 0; --p) + { + var pair = pendingSourceChanges[p]; + List<TextChange> sourceChanges = pair.Item2; + int accumulatedDelta = 0; + for (int sc = 0; sc < sourceChanges.Count - 1; ++sc) // skip the sentinel + { + InterpretSourceBufferChange(pair.Item1, sourceChanges[sc], projectedChanges, urPoints, spanPreAdjustments, spanPostAdjustments, accumulatedDelta); + accumulatedDelta += sourceChanges[sc].Delta; + } + } + + if (this.editApplicationInProgress && (spanPreAdjustments.Count > 0 || spanPostAdjustments.Count > 0)) + { + // We may be generating several distinct versions, so we have to do some position normalization. + // This only occurs in edits originating on this buffer, not independent changes to source buffers. + CorrectSpanAdjustments(projectedChanges, spanPreAdjustments, spanPostAdjustments); + } + + return NormalizedTextChangeCollection.Create(projectedChanges); + } + + private List<Tuple<ITextBuffer, List<TextChange>>> PreparePendingChanges() + { + // Changes to source buffers are interpreted against the state of the projection buffer + // at the beginning of the transaction. However, events raised by source buffers have + // been normalized so that coordinates of later versions are expressed in terms of the + // immediately preceding version, not the version at the beginning of the transaction. + // Therefore we denormalize the changes before considering them here. This has effect only + // when more than one new version has been created for a particular source buffer. + + List<Tuple<ITextBuffer, List<TextChange>>> pendingSourceChanges = new List<Tuple<ITextBuffer, List<TextChange>>>(); + foreach (TextContentChangedEventArgs args in this.pendingContentChangedEventArgs) + { + ITextBuffer sourceBuffer = args.Before.TextBuffer; + + // Get the list of pending changes associated with the source buffer. In the case of an + // independent change to the source buffer, this list is always empty. For dependent changes, + // the list can be nonempty if there are multiple routes to the source buffer. + // In the usual case there will be only one change event for a source buffer so this + // list will not be found. + List<TextChange> bufferChanges; + var pair = pendingSourceChanges.Find((p) => (p.Item1 == sourceBuffer)); + if (pair == null) + { + bufferChanges = new List<TextChange>(1) { new TextChange(int.MaxValue, StringRebuilder.Empty, StringRebuilder.Empty, LineBreakBoundaryConditions.None) }; + pendingSourceChanges.Add(new Tuple<ITextBuffer, List<TextChange>>(sourceBuffer, bufferChanges)); + } + else + { + bufferChanges = pair.Item2; + } + + NormalizedTextChangeCollection.Denormalize(args.Changes, bufferChanges); + } + this.pendingContentChangedEventArgs.Clear(); + return pendingSourceChanges; + } + + private static FrugalList<Tuple<TextChange, int>> Load(SortedDictionary<int, SpanAdjustment> adjustments) + { + FrugalList<Tuple<TextChange, int>> result = new FrugalList<Tuple<TextChange, int>>(); + + foreach (SpanAdjustment adjustment in adjustments.Values) + { + int fudge = 0; + if (adjustment.LeadingChange != null) + { + result.Add(new Tuple<TextChange, int>(adjustment.LeadingChange, 0)); + fudge = adjustment.LeadingChange.Delta; + } + if (adjustment.TrailingChange != null) + { + result.Add(new Tuple<TextChange, int>(adjustment.TrailingChange, fudge)); + } + } + return result; + } + + private static void CorrectSpanAdjustments(List<TextChange> ordinaryChanges, + SortedDictionary<int, SpanAdjustment> spanPreAdjustments, + SortedDictionary<int, SpanAdjustment> spanPostAdjustments) + { + TextChange[] sortedOrdinary = TextUtilities.StableSort(ordinaryChanges, (left, right) => (left.OldPosition - right.OldPosition)); + + FrugalList<TextChange> ordinary = new FrugalList<TextChange>(sortedOrdinary); + FrugalList<Tuple<TextChange, int>> preAdjustments = Load(spanPreAdjustments); + FrugalList<Tuple<TextChange, int>> postAdjustments = Load(spanPostAdjustments); + + int count = ordinary.Count + preAdjustments.Count + postAdjustments.Count; + + TextChange sentinel = new TextChange(int.MaxValue, StringRebuilder.Empty, StringRebuilder.Empty, LineBreakBoundaryConditions.None); + ordinary.Add(sentinel); + preAdjustments.Add(new Tuple<TextChange, int>(sentinel, 0)); + postAdjustments.Add(new Tuple<TextChange, int>(sentinel, 0)); + + int ordDelta = 0; + int preDelta = 0; + int postDelta = 0; + + int ordIndex = 0; + int preIndex = 0; + int postIndex = 0; + + int ordPos = ordinary[0].OldPosition; + int prePos = preAdjustments[0].Item1.OldPosition; + int postPos = postAdjustments[0].Item1.OldPosition; + + for (int c = 0; (c < count); ++c) + { + if (postPos <= prePos && postPos <= ordPos) + { + // post either comes first or is tied. Advance it one step since its adjustments won't affect any of the other adjustments + // (they don't depend on postDelta). + postAdjustments[postIndex].Item1.OldPosition += preDelta + ordDelta + postDelta - postAdjustments[postIndex].Item2; + postAdjustments[postIndex].Item1.NewPosition = postAdjustments[postIndex].Item1.OldPosition; + postDelta += postAdjustments[postIndex].Item1.Delta; + postPos = postAdjustments[++postIndex].Item1.OldPosition; + } + else if (ordPos <= prePos) + { + // The ordinary change comes before or is tied with pre and comes before post. Advance it one step since its adjustment won't + // affect pre's adjustment. + + // postPos comes after ordPos or prePos. + // if ordPos is before prePos, then it must also come before postPos. + Debug.Assert(ordPos < postPos); + + ordinary[ordIndex].OldPosition += preDelta; + ordinary[ordIndex].NewPosition = ordinary[ordIndex].OldPosition; + ordDelta += ordinary[ordIndex].Delta; + ordPos = ordinary[++ordIndex].OldPosition; + } + else + { + Debug.Assert(prePos < ordPos); + Debug.Assert(prePos < postPos); + + preAdjustments[preIndex].Item1.OldPosition += preDelta - preAdjustments[preIndex].Item2; + preAdjustments[preIndex].Item1.NewPosition = preAdjustments[preIndex].Item1.OldPosition; + preDelta += preAdjustments[preIndex].Item1.Delta; + prePos = preAdjustments[++preIndex].Item1.OldPosition; + } + } + + Debug.Assert(ordIndex == ordinary.Count - 1); + Debug.Assert(preIndex == preAdjustments.Count - 1); + Debug.Assert(postIndex == postAdjustments.Count - 1); + } + + + /// <summary> + /// Figure out how a source buffer change affects the projection buffer. + /// </summary> + /// <param name="changedBuffer">The source buffer.</param> + /// <param name="change">The TextChange received from the source buffer.</param> + /// <param name="projectedChanges">List of changes to projection buffer to be augmented if <paramref name="change"/> + /// applies to the source buffer and is not otherwised covered by a span adjustment.</param> + /// <param name="urPoints"></param> + /// <param name="spanPreAdjustments">Adjustments to be manifested as shrinkage of EdgeExclusive source spans before adopting new source snapshot(s).</param> + /// <param name="spanPostAdjustments">Adjustments to be manifested as growth of EdgeExclusive source spans adopting new source snapshot(s).</param> + private void InterpretSourceBufferChange(ITextBuffer changedBuffer, + ITextChange change, + List<TextChange> projectedChanges, + HashSet<SnapshotPoint> urPoints, + SortedDictionary<int, SpanAdjustment> spanPreAdjustments, + SortedDictionary<int, SpanAdjustment> spanPostAdjustments, + int accumulatedDelta) + { + ProjectionSnapshot priorSnapshot = this.currentProjectionSnapshot; + int sourceChangePosition = change.NewPosition; + Span deletionSpan = new Span(sourceChangePosition, change.OldLength); + int insertionCount = change.NewLength; + int cumulativeLength = 0; + + int spanPosition = 0; + + ITextSnapshot afterSourceSnapshot = changedBuffer.CurrentSnapshot; + // todo: consider whether need to use a more precise snapshot. I don't think so, but give it more thought. + // this is used only for mapping to urPoints. + + // This algorithm does a linear search of source spans in forward order. + + foreach (ITrackingSpan sourceSpan in this.sourceSpans) + { + SnapshotSpan priorRawSpan = priorSnapshot.GetSourceSpan(spanPosition); + // Note: if we switch back to not generating a new snapshot of the projection buffer on every source + // buffer change, then here we have to be careful to map the priorRawSpan to the current snapshot of the source buffer, + // since it might be coming from an old snapshot (see e.g. Edit00 unit test) + + if (sourceSpan.TextBuffer == changedBuffer) + { + SpanTrackingMode mode = sourceSpan.TrackingMode; + // is there an easy way to handle custom spans here? + Span? deletedHere = deletionSpan.Overlap(priorRawSpan); + // n.b.: Null span does not overlap with anything + if (deletedHere.HasValue && deletedHere.Value.Length > 0) + { + // part or all of the source span was deleted by the change + + // compute the position at which the change takes place in the projection buffer + // with respect to its current snapshot + int projectedPosition = cumulativeLength + deletedHere.Value.Start - priorRawSpan.Start; + + Debug.Assert(projectedPosition >= 0 && projectedPosition <= priorSnapshot.Length); + + StringRebuilder deletedText = TextChange.ChangeOldSubText(change, Math.Max(priorRawSpan.Start.Position - deletionSpan.Start, 0), deletedHere.Value.Length); + StringRebuilder insertedText = StringRebuilder.Empty; + + SnapshotSpan adjustedPriorRawSpan = new SnapshotSpan(priorRawSpan.Snapshot, priorRawSpan.Start, priorRawSpan.Length - deletedText.Length); + + if (sourceSpan.TrackingMode != SpanTrackingMode.EdgeInclusive && sourceSpan.TrackingMode != SpanTrackingMode.Custom && this.editInProgress) + { + // the tricky cases. If the deletion touches the edge of the source span, we first explicitly + // shrink the span to effect the deletion. If the change is later undone, the source span + // will be grown explicitly to encompass the restored text (otherwise we would lose it since + // the source span is EdgeExclusive and won't grow on its own). + if ((sourceSpan.TrackingMode != SpanTrackingMode.EdgeNegative) && (deletedHere.Value.Start == priorRawSpan.Start)) + { + // A prefix of the source span (or the whole thing) is to be shrunk to effect the deletion. + SpanAdjustment adjust = GetAdjustment(spanPreAdjustments, spanPosition); + // Create the text change that will be induced by the span adjustment + Debug.Assert(adjust.LeadingChange == null); // there can only be one leading change for a particular span + adjust.LeadingChange = TextChange.Create(projectedPosition, deletedText, string.Empty, this.currentProjectionSnapshot); + Debug.Assert(adjust.LeadingChange.OldEnd <= priorSnapshot.Length); + deletedText = StringRebuilder.Empty; + } + else if ((sourceSpan.TrackingMode != SpanTrackingMode.EdgePositive) && (deletedHere.Value.End == priorRawSpan.End)) + { + // A suffix of the source span is to be shrunk to effect the deletion. + SpanAdjustment adjust = GetAdjustment(spanPreAdjustments, spanPosition); + // Create the text change that will be induced by the span adjustment + Debug.Assert(adjust.TrailingChange == null); + adjust.TrailingChange = TextChange.Create(projectedPosition, deletedText, string.Empty, this.currentProjectionSnapshot); + Debug.Assert(adjust.TrailingChange.OldEnd <= priorSnapshot.Length); + deletedText = StringRebuilder.Empty; + } + } + + if (change.NewLength > 0) // change includes an insertion + { + insertedText = InsertionLiesInSpan(afterSourceSnapshot, projectedPosition, spanPosition, adjustedPriorRawSpan, deletionSpan, + sourceChangePosition, mode, change, urPoints, spanPostAdjustments, accumulatedDelta); + if (insertedText.Length > 0) + { + // replacement string is inserted here. There can be more than one insertion + // per change if custom tracking spans are involved. + insertionCount = change.NewLength - insertedText.Length; + } + } + + if (deletedText.Length > 0 || insertedText.Length > 0) + { + TextChange interpretedChange = TextChange.Create(projectedPosition, deletedText, insertedText, this.currentProjectionSnapshot); + Debug.Assert(interpretedChange.OldEnd <= priorSnapshot.Length); + projectedChanges.Add(interpretedChange); + } + } + else if (insertionCount > 0) + { + int projectedPosition = cumulativeLength + Math.Max(sourceChangePosition - priorRawSpan.Start, 0); + // if the insertion is part of a replacement and the source span in question is edge inclusive, a sourceChangePosition to the + // left of the current source span may actually end up being interesting, in which case it would be at the beginning of the span. + // If those conditions don't obtain, InsertionLiesInSpan will return false and nobody will be the wiser. + int hack = spanPostAdjustments == null ? 0 : spanPostAdjustments.Count; + StringRebuilder insertedText = InsertionLiesInSpan(afterSourceSnapshot, projectedPosition, spanPosition, priorRawSpan, deletionSpan, + sourceChangePosition, mode, change, urPoints, spanPostAdjustments, accumulatedDelta); + if (insertedText.Length > 0) + { + // a pure insertion into the source span + + TextChange interpretedChange = TextChange.Create(projectedPosition, string.Empty, insertedText, this.currentProjectionSnapshot); + projectedChanges.Add(interpretedChange); + } + if (spanPostAdjustments != null && spanPostAdjustments.Count != hack) // ur points should have eliminated the need for the hack + { + insertionCount = 0; + } + } + } + cumulativeLength += priorRawSpan.Length; + spanPosition++; + } + } + + private StringRebuilder InsertionLiesInSpan(ITextSnapshot afterSourceSnapshot, + int projectedPosition, + int spanPosition, + SnapshotSpan rawSpan, + Span deletionSpan, + int sourcePosition, + SpanTrackingMode mode, + ITextChange incomingChange, + HashSet<SnapshotPoint> urPoints, + SortedDictionary<int, SpanAdjustment> spanAdjustments, + int accumulatedDelta) + { + int renormalizedSourcePosition = sourcePosition + accumulatedDelta; + if (mode == SpanTrackingMode.Custom) + { + return InsertionLiesInCustomSpan(afterSourceSnapshot, spanPosition, incomingChange, urPoints, accumulatedDelta); + } + + bool contains = rawSpan.Contains(sourcePosition); + if (mode == SpanTrackingMode.EdgeInclusive) + { + if ((this.bufferOptions & ProjectionBufferOptions.PermissiveEdgeInclusiveSourceSpans) != 0) + { + return InsertionLiesInPermissiveInclusiveSpan + (afterSourceSnapshot, rawSpan, deletionSpan, sourcePosition, renormalizedSourcePosition, incomingChange, urPoints); + } + else + { + return contains || sourcePosition == rawSpan.End ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty; + } + } + else + { + if (!this.editInProgress) + { + // Edit originated in the source buffer; we don't do any implicit growing + // of spans + bool included; + if (mode == SpanTrackingMode.EdgeNegative) + { + included = contains; + } + else if (mode == SpanTrackingMode.EdgePositive) + { + included = (sourcePosition != rawSpan.Start) && (contains || sourcePosition == rawSpan.End); + } + else + { + included = contains && sourcePosition != rawSpan.Start; + } + return included ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty; + } + else + { + if (sourcePosition == rawSpan.Start && (mode != SpanTrackingMode.EdgeNegative)) + { + SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), + (buffer) => (buffer is TextBuffer), + PositionAffinity.Successor); + Debug.Assert(urPoint.HasValue); + if (urPoints.Add(urPoint.Value)) + { + if (BufferGroup.Tracing) + { + Debug.WriteLine("UR-Point [exclusive:start]" + urPoint.Value.ToString()); + } + // Insertion at exclusive left edge of source span: we need to grow the source span on the left + SpanAdjustment adjust = GetAdjustment(spanAdjustments, spanPosition); + // Create the text change that will be induced by the span adjustment + Debug.Assert(adjust.LeadingChange == null); + adjust.LeadingChange = TextChange.Create(projectedPosition, StringRebuilder.Empty, TextChange.NewStringRebuilder(incomingChange), this.currentProjectionSnapshot); + } + return StringRebuilder.Empty; // this insertion either already happened or happens on a subsequent transaction, not this one + } + else if (sourcePosition == rawSpan.End && (mode != SpanTrackingMode.EdgePositive)) + { + SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), + (buffer) => (buffer is TextBuffer), + PositionAffinity.Predecessor); + Debug.Assert(urPoint.HasValue); + if (urPoints.Add(urPoint.Value)) + { + if (BufferGroup.Tracing) + { + Debug.WriteLine("UR-Point [exclusive:end]" + urPoint.Value.ToString()); + } + // Insertion at exclusive right edge of source span: we need to grow the source span on the right + SpanAdjustment adjust = GetAdjustment(spanAdjustments, spanPosition); + // Create the text change that will be induced by the span adjustment + Debug.Assert(adjust.TrailingChange == null); + adjust.TrailingChange = TextChange.Create(projectedPosition, StringRebuilder.Empty, TextChange.NewStringRebuilder(incomingChange), this.currentProjectionSnapshot); + } + return StringRebuilder.Empty; // this insertion either already happened or happens on a subsequent transaction, not this one + } + return (contains || (mode == SpanTrackingMode.EdgePositive && sourcePosition == rawSpan.End)) ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty; + } + } + } + + private StringRebuilder InsertionLiesInCustomSpan(ITextSnapshot afterSourceSnapshot, + int spanPosition, + ITextChange incomingChange, + HashSet<SnapshotPoint> urPoints, + int accumulatedDelta) + { + // just evaluate the new span and see if it overlaps the insertion. + ITrackingSpan sourceTrackingSpan = this.sourceSpans[spanPosition]; + SnapshotSpan afterSpan = sourceTrackingSpan.GetSpan(afterSourceSnapshot); + + Span newSpan = new Span(incomingChange.NewPosition + accumulatedDelta, incomingChange.NewLength); + Span? over = newSpan.Overlap(afterSpan); + return over.HasValue ? BufferFactoryService.StringRebuilderFromSnapshotAndSpan(afterSourceSnapshot, over.Value) : StringRebuilder.Empty; + + + //if (futureSpan.Contains(renormalizedSourcePosition)) + //{ + // if (BufferGroup.Tracing) + // { + // Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, + // "Custom renormPosition {0} priorSpan [{1}..{2})", renormalizedSourcePosition, priorSpan.Start.Position + accumulatedDelta, priorSpan.End.Position + accumulatedDelta)); + // } + // if (renormalizedSourcePosition == (priorSpan.Start.Position + accumulatedDelta) || renormalizedSourcePosition == (priorSpan.End.Position + accumulatedDelta)) + // { + // SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), + // (buffer) => (buffer is TextBuffer), + // renormalizedSourcePosition == priorSpan.Start.Position + accumulatedDelta + // ? PositionAffinity.Successor + // : PositionAffinity.Predecessor); + // Debug.Assert(urPoint.HasValue); + // bool added = urPoints.Add(urPoint.Value); + // Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point + // if (BufferGroup.Tracing) + // { + // Debug.WriteLine("UR-Point [custom]" + urPoint.Value.ToString()); + // } + // } + // return true; + //} + //else + //{ + // return false; + //} + } + + private static StringRebuilder InsertionLiesInPermissiveInclusiveSpan(ITextSnapshot afterSourceSnapshot, + SnapshotSpan rawSpan, + Span deletionSpan, + int sourcePosition, + int renormalizedSourcePosition, + ITextChange incomingChange, + HashSet<SnapshotPoint> urPoints) + { + bool leadingInclusiveGrowth = sourcePosition < rawSpan.Start && deletionSpan.End >= rawSpan.Start; + if (sourcePosition == rawSpan.Start || leadingInclusiveGrowth) + { + SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), + (buffer) => (buffer is TextBuffer), + PositionAffinity.Successor); + Debug.Assert(urPoint.HasValue); + bool added = urPoints.Add(urPoint.Value); + Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point + if (BufferGroup.Tracing) + { + Debug.WriteLine("UR-Point [inclusive:start]" + urPoint.Value.ToString()); + } + return TextChange.NewStringRebuilder(incomingChange); + } + else if (sourcePosition == rawSpan.End) + { + SnapshotPoint? urPoint = MappingHelper.MapDownToFirstMatchNoTrack(new SnapshotPoint(afterSourceSnapshot, renormalizedSourcePosition), + (buffer) => (buffer is TextBuffer), + PositionAffinity.Predecessor); + Debug.Assert(urPoint.HasValue); + bool added = urPoints.Add(urPoint.Value); + Debug.Assert(added); // if this is false we are sorta hosed - we already handled this point + if (BufferGroup.Tracing) + { + Debug.WriteLine("UR-Point [inclusive:end]" + urPoint.Value.ToString()); + } + return TextChange.NewStringRebuilder(incomingChange); + } + else + { + return rawSpan.Contains(sourcePosition) ? TextChange.NewStringRebuilder(incomingChange) : StringRebuilder.Empty; + } + } + + /// <summary> + /// Fetch <see cref="SpanAdjustment"/> for this span if it already exists, or create a new one. + /// </summary> + private static SpanAdjustment GetAdjustment(SortedDictionary<int, SpanAdjustment> spanAdjustments, int spanPosition) + { + SpanAdjustment adjust; + if (!spanAdjustments.TryGetValue(spanPosition, out adjust)) + { + adjust = new SpanAdjustment(); + spanAdjustments.Add(spanPosition, adjust); + } + return adjust; + } + #endregion + + #region Change Application + /// <summary> + /// Given the set of changes to apply to this buffer, compute the set of changes to apply to its + /// source buffers. These edit objects are managed by the buffer group, which will decide when to + /// apply them. + /// </summary> + private void ComputeSourceEdits(FrugalList<TextChange> changes) + { + foreach (TextChange change in changes) + { + if (change.OldLength > 0 && change.NewLength == 0) + { + // the change is a deletion + IList<SnapshotSpan> sourceDeletionSpans = this.currentProjectionSnapshot.MapToSourceSnapshots(new Span(change.NewPosition, change.OldLength)); + foreach (SnapshotSpan sourceDeletionSpan in sourceDeletionSpans) + { + DeleteFromSource(sourceDeletionSpan); + } + } + + else if (change.OldLength > 0 && change.NewLength > 0) + { + // the change is a replacement + ReadOnlyCollection<SnapshotSpan> allSourceReplacementSpans = + this.currentProjectionSnapshot.MapReplacementSpanToSourceSnapshots + (new Span(change.OldPosition, change.OldLength), (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0 ? this.literalBuffer : null); + + //Filter out replacement spans that are read-only (since we couldn't edit them in any case). + FrugalList<SnapshotSpan> sourceReplacementSpans = new FrugalList<SnapshotSpan>(); + foreach (var s in allSourceReplacementSpans) + { + if (!s.Snapshot.TextBuffer.IsReadOnly(s.Span, true)) + sourceReplacementSpans.Add(s); + } + + Debug.Assert(sourceReplacementSpans.Count > 0); // if replacement is on read-only buffers, the read only check will have already caught it + + if (sourceReplacementSpans.Count == 1) + { + ReplaceInSource(sourceReplacementSpans[0], change.NewText, 0 + change.MasterChangeOffset); + } + else + { + // the replacement hits the boundary of source spans + int[] insertionSizes = new int[sourceReplacementSpans.Count]; + + if (this.resolver != null) + { + SnapshotSpan projectionReplacementSpan = new SnapshotSpan(this.currentProjectionSnapshot, change.OldPosition, change.OldLength); + this.resolver.FillInReplacementSizes(projectionReplacementSpan, new ReadOnlyCollection<SnapshotSpan>(sourceReplacementSpans), change.NewText, insertionSizes); + if (BufferGroup.Tracing) + { + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, + "## Seam Replacement @:{0}", projectionReplacementSpan)); + for (int s = 0; s < sourceReplacementSpans.Count; ++s) + { + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, + "## {0,4}: {1}", insertionSizes[s], sourceReplacementSpans[s])); + } + Debug.WriteLine(string.Format(System.Globalization.CultureInfo.CurrentCulture, + "## Replacement Text:'{0}'", TextUtilities.Escape(change.NewText))); + } + } + insertionSizes[insertionSizes.Length - 1] = int.MaxValue; + + int pos = 0; + for (int i = 0; i < insertionSizes.Length; ++i) + { + // contend with any old garbage that the client passed back. + int insertionSize = Math.Min(insertionSizes[i], change.NewLength - pos); + if (insertionSize > 0) + { + ReplaceInSource(sourceReplacementSpans[i], TextChange.ChangeNewSubstring(change, pos, insertionSize), pos + change.MasterChangeOffset); + pos += insertionSize; + } + else if (sourceReplacementSpans[i].Length > 0) + { + DeleteFromSource(sourceReplacementSpans[i]); + } + } + } + } + else + { + Debug.Assert(change.OldLength == 0 && change.NewLength > 0); + // the change is an insertion + ReadOnlyCollection<SnapshotPoint> allSourceInsertionPoints = + this.currentProjectionSnapshot.MapInsertionPointToSourceSnapshots + (change.NewPosition, (this.bufferOptions & ProjectionBufferOptions.WritableLiteralSpans) == 0 ? this.literalBuffer : null); + + Debug.Assert(allSourceInsertionPoints.Count > 0); // if insertion point is between two literal spans, the read only check will have already caught it + + //Filter out replacement spans that are read-only (since we couldn't edit them in any case). + FrugalList<SnapshotPoint> sourceInsertionPoints = new FrugalList<SnapshotPoint>(); + foreach (var p in allSourceInsertionPoints) + { + if (!p.Snapshot.TextBuffer.IsReadOnly(p.Position, true)) + sourceInsertionPoints.Add(p); + } + + Debug.Assert(sourceInsertionPoints.Count > 0); // if insertion point is between only read-only buffers, the read only check will have already caught it + + if (sourceInsertionPoints.Count == 1) + { + // the insertion point is unambiguous + InsertInSource(sourceInsertionPoints[0], change.NewText, 0 + change.MasterChangeOffset); + } + else + { + // the insertion is at the boundary of source spans + int[] insertionSizes = new int[sourceInsertionPoints.Count]; + + if (this.resolver != null) + { + this.resolver.FillInInsertionSizes(new SnapshotPoint(this.currentProjectionSnapshot, change.NewPosition), + new ReadOnlyCollection<SnapshotPoint>(sourceInsertionPoints), change.NewText, insertionSizes); + } + + // if resolver was not provided, we just use zeros for the insertion sizes, which will push the entire insertion + // into the last slot. + insertionSizes[insertionSizes.Length - 1] = int.MaxValue; + + int pos = 0; + for (int i = 0; i < insertionSizes.Length; ++i) + { + // contend with any old garbage that the client passed back. + int size = Math.Min(insertionSizes[i], change.NewLength - pos); + if (size > 0) + { + InsertInSource(sourceInsertionPoints[i], change._newText.GetText(new Span(pos, size)), pos + change.MasterChangeOffset); + pos += size; + if (pos == change.NewLength) + { + break; // inserted text is used up, whether we've visited all of the insertionSizes or not + } + } + } + } + } + } + // defer interpretation of events that will be raised by source buffers as we make these edits + this.editApplicationInProgress = true; + } + + #endregion + + #region Snapshots + protected override BaseSnapshot TakeSnapshot() + { + List<SnapshotSpan> newSourceSpans = new List<SnapshotSpan>(this.sourceSpans.Count); + foreach (ITrackingSpan sourceSpan in this.sourceSpans) + { + // since we are on the main thread, we can safely just look at current snapshots + newSourceSpans.Add(sourceSpan.GetSpan(sourceSpan.TextBuffer.CurrentSnapshot)); + } + this.currentProjectionSnapshot = MakeSnapshot(newSourceSpans); + return this.currentProjectionSnapshot; + } + + private ProjectionSnapshot TakeStaticSnapshot(List<SnapshotSpan> newSourceSpans) + { + // this form of snapshot uses the same source snapshots as current snapshot rather than current snapshots + return MakeSnapshot(newSourceSpans); + } + + private ProjectionSnapshot MakeSnapshot(List<SnapshotSpan> newSourceSpans) + { + return new ProjectionSnapshot(this, this.currentVersion, this.builder, newSourceSpans); + } + + protected override StringRebuilder GetDoppelgangerBuilder() + { + ITextBuffer doppelBottom; + if (Properties.TryGetProperty<ITextBuffer>("IdentityMapping", out doppelBottom)) + { + var snapshot = doppelBottom.CurrentSnapshot; + return BufferFactoryService.StringRebuilderFromSnapshotAndSpan(snapshot, new Span(0, snapshot.Length)); + } + + if (this.sourceSpans.Count == 1) + { + var sourceSpan = this.sourceSpans[0]; + var source = sourceSpan.GetSpan(sourceSpan.TextBuffer.CurrentSnapshot); + if ((source.Length == this.currentVersion.Length) && (source.Snapshot is BaseSnapshot)) + { + // We're mapped to a single span that is equal to the entire contents the source buffer. + return BufferFactoryService.StringRebuilderFromSnapshotSpan(source); + } + } + + return null; + } + + public override IProjectionSnapshot CurrentSnapshot + { + get { return this.currentProjectionSnapshot; } + } + + protected override BaseProjectionSnapshot CurrentBaseSnapshot + { + get { return this.currentProjectionSnapshot; } + } + #endregion + + #region Events + internal event EventHandler<ProjectionSourceBuffersChangedEventArgs> SourceBuffersChangedImmediate; + internal event EventHandler<ProjectionSourceSpansChangedEventArgs> SourceSpansChangedImmediate; + + public event EventHandler<ProjectionSourceBuffersChangedEventArgs> SourceBuffersChanged; + public event EventHandler<ProjectionSourceSpansChangedEventArgs> SourceSpansChanged; + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs b/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs new file mode 100644 index 0000000..5414af1 --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/ProjectionSnapshot.cs @@ -0,0 +1,757 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Diagnostics; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + + using Strings = Microsoft.VisualStudio.Text.Implementation.Strings; + + internal partial class ProjectionSnapshot : BaseProjectionSnapshot, IProjectionSnapshot + { + #region Private members + private readonly ProjectionBuffer projectionBuffer; + private ReadOnlyCollection<SnapshotSpan> sourceSpans; + private ReadOnlyCollection<ITextSnapshot> sourceSnapshots; + + + /// <summary> + /// Denotes a source span and its character position in the projection snapshot. + /// </summary> + struct InvertedSource + { + public readonly Span sourceSpan; + public readonly int projectedPosition; + public InvertedSource(Span sourceSpan, int projectedPosition) + { + this.sourceSpan = sourceSpan; + this.projectedPosition = projectedPosition; + } + } + + /// <summary> + /// For each source snapshot, a list of the source spans from that snapshot, plus their positions in the + /// projection snapshot, ordered by their positions in the source snapshot. This enables fast mapping from + /// source snapshot positions/spans to the equivalent in this projection snapshot. For projection buffers + /// with many source spans, the (time) overhead of creating this structure is about 10% of the snapshot cost. + /// </summary> + private Dictionary<ITextSnapshot, List<InvertedSource>> sourceSnapshotMap; + + private int[] cumulativeLineBreakCounts; + private int[] cumulativeLengths; + #endregion + + #region Construction + public ProjectionSnapshot(ProjectionBuffer projectionBuffer, ITextVersion2 version, StringRebuilder content, IList<SnapshotSpan> sourceSpans) + : base(version, content) + { + this.projectionBuffer = projectionBuffer; + this.sourceSpans = new ReadOnlyCollection<SnapshotSpan>(sourceSpans); + + this.cumulativeLengths = new int[sourceSpans.Count + 1]; + this.cumulativeLineBreakCounts = new int[sourceSpans.Count + 1]; + + this.sourceSnapshotMap = new Dictionary<ITextSnapshot, List<InvertedSource>>(); + for (int s = 0; s < sourceSpans.Count; ++s) + { + SnapshotSpan sourceSpan = sourceSpans[s]; + this.totalLength += sourceSpan.Length; + + this.cumulativeLengths[s + 1] = this.cumulativeLengths[s] + sourceSpan.Length; + + // Most source spans won't change when generating a new projection snapshot, + // which means we should be able to reuse the line break count from the previous + // projection snapshot. + + int lineBreakCount = sourceSpan.Snapshot.GetLineNumberFromPosition(sourceSpan.End) - sourceSpan.Snapshot.GetLineNumberFromPosition(sourceSpan.Start); + + // todo: incorrect when span ends with \r and following begins with \n + this.totalLineCount += lineBreakCount; + + this.cumulativeLineBreakCounts[s + 1] = this.cumulativeLineBreakCounts[s] + lineBreakCount; + + ITextSnapshot snapshot = sourceSpan.Snapshot; + List<InvertedSource> invertedSources; + if (!this.sourceSnapshotMap.TryGetValue(snapshot, out invertedSources)) + { + invertedSources = new List<InvertedSource>(); + this.sourceSnapshotMap.Add(snapshot, invertedSources); + } + invertedSources.Add(new InvertedSource(sourceSpan.Span, this.cumulativeLengths[s])); + } + + // The SourceSnapshots property is heavily used, so calculate it once + this.sourceSnapshots = new ReadOnlyCollection<ITextSnapshot>(new List<ITextSnapshot>(this.sourceSnapshotMap.Keys)); + + // sort the per-buffer source span lists by position in source snapshot + foreach (var v in this.sourceSnapshotMap.Values) + { + // sort by starting position. Spans can't overlap, but we do need null spans at a particular position + // to precede non-null spans at that position, so if starting positions are equal, compare the ends. + v.Sort((left, right) => (left.sourceSpan.Start == right.sourceSpan.Start + ? left.sourceSpan.End - right.sourceSpan.End + : left.sourceSpan.Start - right.sourceSpan.Start)); + } + + if (BufferGroup.Tracing) + { + Debug.WriteLine(LocalToString()); + } + if (this.totalLength != version.Length) + { + Debug.Fail(string.Format(System.Globalization.CultureInfo.CurrentCulture, + "Projection Snapshot Inconsistency. Sum of spans: {0}, Previous + delta: {1}", this.totalLength, version.Length)); + throw new InvalidOperationException(Strings.InvalidLengthCalculation); + } + OverlapCheck(); + } + + private void OverlapCheck() + { + // try to diagnose problem with overlapping spans + Dictionary<ITextSnapshot, List<Span>> groundSourceSpansMap = new Dictionary<ITextSnapshot, List<Span>>(); + MapDownToGround(this.sourceSpans, groundSourceSpansMap); + + foreach (KeyValuePair<ITextSnapshot, List<Span>> kvp in groundSourceSpansMap) + { + int sum = 0; + foreach (Span s in kvp.Value) + { + sum += s.Length; + } + NormalizedSpanCollection norma = new NormalizedSpanCollection(kvp.Value); + int normaSum = 0; + foreach (Span s in norma) + { + normaSum += s.Length; + } + Debug.Assert(sum == normaSum); + if (sum != normaSum) + { + throw new InvalidOperationException(Strings.OverlappingSourceSpans); + } + } + } + + private static void MapDownToGround(IList<SnapshotSpan> spans, Dictionary<ITextSnapshot, List<Span>> groundSourceSpansMap) + { + foreach (SnapshotSpan span in spans) + { + IProjectionSnapshot projSnap = span.Snapshot as IProjectionSnapshot; + if (projSnap == null) + { + List<Span> groundSpans; + if (!groundSourceSpansMap.TryGetValue(span.Snapshot, out groundSpans)) + { + groundSpans = new List<Span>(); + groundSourceSpansMap.Add(span.Snapshot, groundSpans); + } + groundSpans.Add(span); + } + else + { + MapDownToGround(projSnap.MapToSourceSnapshots(span), groundSourceSpansMap); + } + } + } + #endregion + + #region Buffers and Spans + public override IProjectionBufferBase TextBuffer + { + get { return this.projectionBuffer; } + } + + protected override ITextBuffer TextBufferHelper + { + get { return this.projectionBuffer; } + } + + public override int SpanCount + { + get { return this.sourceSpans.Count; } + } + + public override ReadOnlyCollection<ITextSnapshot> SourceSnapshots + { + get { return this.sourceSnapshots; } + } + + public override ITextSnapshot GetMatchingSnapshot(ITextBuffer textBuffer) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys) + { + if (snappy.TextBuffer == textBuffer) + { + return snappy; + } + } + return null; + } + + public override ITextSnapshot GetMatchingSnapshotInClosure(ITextBuffer textBuffer) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys) + { + if (snappy.TextBuffer == textBuffer) + { + return snappy; + } + IProjectionSnapshot2 projSnappy = snappy as IProjectionSnapshot2; + if (projSnappy is IProjectionSnapshot2) + { + ITextSnapshot maybe = projSnappy.GetMatchingSnapshotInClosure(textBuffer); + if (maybe != null) + { + return maybe; + } + } + } + return null; + } + + public override ITextSnapshot GetMatchingSnapshotInClosure(Predicate<ITextBuffer> match) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + foreach (ITextSnapshot snappy in this.sourceSnapshotMap.Keys) + { + if (match(snappy.TextBuffer)) + { + return snappy; + } + IProjectionSnapshot2 projSnappy = snappy as IProjectionSnapshot2; + if (projSnappy is IProjectionSnapshot2) + { + ITextSnapshot maybe = projSnappy.GetMatchingSnapshotInClosure(match); + if (maybe != null) + { + return maybe; + } + } + } + return null; + } + + public override ReadOnlyCollection<SnapshotSpan> GetSourceSpans() + { + return this.sourceSpans; + } + + public override ReadOnlyCollection<SnapshotSpan> GetSourceSpans(int startSpanIndex, int count) + { + if (startSpanIndex < 0 || startSpanIndex > this.SpanCount) + { + throw new ArgumentOutOfRangeException("startSpanIndex"); + } + if (count < 0 || startSpanIndex + count > this.SpanCount) + { + throw new ArgumentOutOfRangeException("count"); + } + + // better using iterator or explicit successor func eventually + List<SnapshotSpan> resultSpans = new List<SnapshotSpan>(count); + for (int i = 0; i < count; ++i) + { + resultSpans.Add(this.sourceSpans[startSpanIndex + i]); + } + return new ReadOnlyCollection<SnapshotSpan>(resultSpans); + } + + internal SnapshotSpan GetSourceSpan(int position) + { + return this.sourceSpans[position]; + } + #endregion + + #region Mapping + public override ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshotsForRead(Span span) + { + if (span.End > this.Length) + { + throw new ArgumentOutOfRangeException("span"); + } + + FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>(); + + if (span.Length == 0) + { + // First check for a degenerate snapshot having no source spans + if (span.Start == 0 && this.sourceSpans.Count == 0) + { + return new ReadOnlyCollection<SnapshotSpan>(mappedSpans); + } + + // Zero-length spans are special in that they may map to more than one zero-length source span. + // Defer to the point mapping implementation and then convert back to spans. + ReadOnlyCollection<SnapshotPoint> points = MapInsertionPointToSourceSnapshots(span.Start, null); + for (int p = 0; p < points.Count; ++p) + { + SnapshotPoint point = points[p]; + SnapshotSpan mappedSpan = new SnapshotSpan(point.Snapshot, point.Position, 0); + // avoid duplicates, caused by mapping the null span on a seam between source spans + // that come from the same source buffer and are adjacent in that source buffer + // Example: source spans are [0..10) and [10..20) from same source buffer, and we + // are requested to map the span at the seam, corresponding to [10..10). + if (mappedSpans.Count == 0 || mappedSpan != mappedSpans[mappedSpans.Count - 1]) + { + mappedSpans.Add(mappedSpan); + } + } + } + else + { + int rover = FindHighestSpanIndexOfPosition(span.Start); + // sourceSpans[rover] contains span.Start + + SnapshotSpan sourceSpan = this.sourceSpans[rover]; + SnapshotPoint mappedStart = sourceSpan.Start + (span.Start - this.cumulativeLengths[rover]); + int mappedLength = mappedStart.Position + span.Length < sourceSpan.End ? span.Length : sourceSpan.End.Position - mappedStart; + mappedSpans.Add(new SnapshotSpan(mappedStart, mappedLength)); + + // walk forward until we cover the entire span + while (mappedLength < span.Length) + { + sourceSpan = this.sourceSpans[++rover]; + if (span.End >= this.cumulativeLengths[rover + 1]) + { + mappedLength += sourceSpan.Length; + mappedSpans.Add(sourceSpan); + } + else + { + mappedLength += span.End - this.cumulativeLengths[rover]; + mappedSpans.Add(new SnapshotSpan(sourceSpan.Snapshot, new Span(sourceSpan.Start, span.End - this.cumulativeLengths[rover]))); + } + } + } + + return new ReadOnlyCollection<SnapshotSpan>(mappedSpans); + } + + public override ReadOnlyCollection<SnapshotSpan> MapToSourceSnapshots(Span span) + { + return MapToSourceSnapshotsForRead(span); + } + + internal override ReadOnlyCollection<SnapshotSpan> MapReplacementSpanToSourceSnapshots(Span replacementSpan, ITextBuffer excludedBuffer) + { + // This is exactly like ordinary span mapping, except that empty source spans at the ends of the replacementSpan are included. + + FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>(); + int rover = FindLowestSpanIndexOfPosition(replacementSpan.Start); + int roverHi = FindHighestSpanIndexOfPosition(replacementSpan.End); + // sourceSpans[rover] contains span.Start + + SnapshotSpan sourceSpan = this.sourceSpans[rover]; + { + SnapshotPoint mappedStart = sourceSpan.Start + (replacementSpan.Start - this.cumulativeLengths[rover]); + int mappedLength = mappedStart.Position + replacementSpan.Length < sourceSpan.End ? replacementSpan.Length : sourceSpan.End.Position - mappedStart; + SnapshotSpan mappedSpan = new SnapshotSpan(mappedStart, mappedLength); + if (mappedSpan.Length > 0 || mappedSpan.Snapshot.TextBuffer != excludedBuffer) + { + mappedSpans.Add(new SnapshotSpan(mappedStart, mappedLength)); + } + } + + // walk forward until we cover the entire span + while (rover < roverHi) + { + rover++; + sourceSpan = this.sourceSpans[rover]; + SnapshotSpan mappedSpan = replacementSpan.End >= this.cumulativeLengths[rover + 1] + ? sourceSpan + : new SnapshotSpan(sourceSpan.Snapshot, new Span(sourceSpan.Start, replacementSpan.End - this.cumulativeLengths[rover])); + if (mappedSpan.Length > 0 || mappedSpan.Snapshot.TextBuffer != excludedBuffer) + { + mappedSpans.Add(mappedSpan); + } + } + + Debug.Assert(replacementSpan.Length == mappedSpans.Sum((SnapshotSpan s) => s.Length), "Inconsistency in MapReplacementSpanToSourceSnapshots"); + + return new ReadOnlyCollection<SnapshotSpan>(mappedSpans); + } + + public override ReadOnlyCollection<Span> MapFromSourceSnapshot(SnapshotSpan sourceSpan) + { + List<InvertedSource> orderedSources; + if (!this.sourceSnapshotMap.TryGetValue(sourceSpan.Snapshot, out orderedSources)) + { + throw new ArgumentException("The span does not belong to a source snapshot of the projection snapshot"); + } + + Span spanToMap = sourceSpan.Span; + + // binary search for source span containing spanToMap.Start + int lo = 0; + int hi = orderedSources.Count - 1; + int mid = 0; + while (lo <= hi) + { + mid = (lo + hi) / 2; + + if (spanToMap.Start < orderedSources[mid].sourceSpan.Start) + { + hi = mid - 1; + } + else if (spanToMap.Start > orderedSources[mid].sourceSpan.End) + { + lo = mid + 1; + } + else + { + break; + // orderedSources[mid].sourceSpan contains (or abuts at the end) sourceSpan.Start + } + } + + FrugalList<Span> result = new FrugalList<Span>(); + + if (spanToMap.Start > orderedSources[mid].sourceSpan.End) + { + Debug.Assert(hi < lo, "Projection source span search exit invariant violated"); + // the binary search failed (hi and lo crossed) because spanToMap.Start did + // not intersect any source span. However, it may be that some part of spanToMap will + // intersect the next span, so we start our scan one span further along. + + // another way to think of this: if the binary search failed, mid will designate either the source span + // to the left of the gap containing start or to the right of the gap containing start. This case + // is where orderedSources[mid] lies to the left of the gap, and we don't want the loop below to blow out on + // the first iteration if spanToMap intersects orderedSources[mid+1]. + mid++; + } + + for (int rover = mid; rover < orderedSources.Count; ++rover) + { + Span? s = spanToMap.Intersection(orderedSources[rover].sourceSpan); + if (!s.HasValue) + { + Debug.Assert(orderedSources[rover].sourceSpan.Start > spanToMap.End); + break; + } + if (s.Value.Length > 0 || spanToMap.Length == 0) + { + result.Add(new Span(orderedSources[rover].projectedPosition + (s.Value.Start - orderedSources[rover].sourceSpan.Start), s.Value.Length)); + } + } + + return new ReadOnlyCollection<Span>(result); + } + + public override SnapshotPoint MapToSourceSnapshot(int position) + { + if (position < 0 || position > this.totalLength) + { + throw new ArgumentOutOfRangeException("position"); + } + ReadOnlyCollection<SnapshotPoint> points = this.MapInsertionPointToSourceSnapshots(position, this.projectionBuffer.literalBuffer); // should this be conditional on writable literal buffer? + if (points.Count == 1) + { + return points[0]; + } + else if (this.projectionBuffer.resolver == null) + { + return points[points.Count - 1]; + } + else + { + return points[this.projectionBuffer.resolver.GetTypicalInsertionPosition(new SnapshotPoint(this, position), points)]; + } + } + + public override SnapshotPoint MapToSourceSnapshot(int position, PositionAffinity affinity) + { + if (position < 0 || position > this.Length) + { + throw new ArgumentOutOfRangeException("position"); + } + if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) + { + throw new ArgumentOutOfRangeException("affinity"); + } + + int rover = affinity == PositionAffinity.Predecessor ? FindLowestSpanIndexOfPosition(position) + : FindHighestSpanIndexOfPosition(position); + if (rover < 0) + { + Debug.Assert(this.sourceSpans.Count == 0); + throw new InvalidOperationException(); + } + + SnapshotSpan roverSpan = this.sourceSpans[rover]; + return roverSpan.Start + (position - cumulativeLengths[rover]); + } + + public override SnapshotPoint? MapFromSourceSnapshot(SnapshotPoint sourcePoint, PositionAffinity affinity) + { + if (affinity < PositionAffinity.Predecessor || affinity > PositionAffinity.Successor) + { + throw new ArgumentOutOfRangeException("affinity"); + } + + List<InvertedSource> orderedSources; + if (!this.sourceSnapshotMap.TryGetValue(sourcePoint.Snapshot, out orderedSources)) + { + throw new ArgumentException("The point does not belong to a source snapshot of the projection snapshot"); + } + + int position = sourcePoint.Position; + SnapshotPoint? candidatePoint = null; + + // binary search for source span containing pointToMap + int lo = 0; + int hi = orderedSources.Count - 1; + while (lo <= hi) + { + int mid = (lo + hi) / 2; + Span sourceSpan = orderedSources[mid].sourceSpan; + + if (position < sourceSpan.Start) + { + hi = mid - 1; + } + else if (position > sourceSpan.End) + { + lo = mid + 1; + } + else + { + candidatePoint = new SnapshotPoint(this, orderedSources[mid].projectedPosition + sourcePoint.Position - sourceSpan.Start); + + if ((position > sourceSpan.Start && position < sourceSpan.End) || + (position == sourceSpan.Start && affinity == PositionAffinity.Successor) || + (position == sourceSpan.End && affinity == PositionAffinity.Predecessor)) + { + // unambiguous + return candidatePoint; + } + + if (position == sourceSpan.Start) + { + hi = mid - 1; + } + else if (position == sourceSpan.End) + { + lo = mid + 1; + } + else + { + Debug.Fail("Ambiguous point mapping unexpected condition"); + break; // we have a decent answer + } + } + } + + return candidatePoint; + } + + /// <summary> + /// Map insertion point in projection buffer into set of insertion points in source buffers. The + /// result will have only one element unless the insertion is at the boundary of source spans, in which + /// case there can be two (or more if empty source spans appear at the insertion location). + /// </summary> + internal override ReadOnlyCollection<SnapshotPoint> MapInsertionPointToSourceSnapshots(int position, ITextBuffer excludedBuffer) + { + if (position < 0 || position > this.Length) + { + throw new ArgumentOutOfRangeException("position"); + } + + int rover = FindLowestSpanIndexOfPosition(position); + + SnapshotSpan sourceSpan = this.sourceSpans[rover]; + if (position < cumulativeLengths[rover + 1]) + { + // point is not on a seam + FrugalList<SnapshotPoint> singleResult = new FrugalList<SnapshotPoint>(); + singleResult.Add(sourceSpan.Start + (position - this.cumulativeLengths[rover])); + return new ReadOnlyCollection<SnapshotPoint>(singleResult); + } + else + { + // point is at the boundary of source spans (this includes being at the + // very beginning or end of the buffer, but that will work out OK). + var sourceInsertionPoints = new FrugalList<SnapshotPoint>(); + + // include the end point of the source span on the left + var firstSnapshotPoint = new SnapshotPoint(sourceSpan.Snapshot, sourceSpan.End); + if (sourceSpan.Snapshot.TextBuffer != excludedBuffer) + { + sourceInsertionPoints.Add(firstSnapshotPoint); + } + + // include all consecutive source spans of zero length (typically there are none of these) + while (++rover < this.sourceSpans.Count && this.cumulativeLengths[rover] == this.cumulativeLengths[rover + 1]) + { + sourceSpan = this.sourceSpans[rover]; + if (sourceSpan.Snapshot.TextBuffer != excludedBuffer) + { + sourceInsertionPoints.Add(new SnapshotPoint(sourceSpan.Snapshot, sourceSpan.Start)); + } + } + + // include first nonzero length source span (if any) + if (rover < this.sourceSpans.Count) + { + sourceSpan = this.sourceSpans[rover]; + if (sourceSpan.Snapshot.TextBuffer != excludedBuffer) + { + sourceInsertionPoints.Add(new SnapshotPoint(sourceSpan.Snapshot, sourceSpan.Start)); + } + } + + if (sourceInsertionPoints.Count == 0) + { + // Where position falls in the seam between two (or more if they are 0 length) spans from excludedBuffer and there + // are no snapshot points from the "real" buffers. In this case, the best thing to do is simply return the span from + // the excluded buffer (which is consistent with our behavior when position falls inside the middle of an excluded span). + sourceInsertionPoints.Add(firstSnapshotPoint); + } + + return new ReadOnlyCollection<SnapshotPoint>(sourceInsertionPoints); + } + } + #endregion + + #region Search + /// <summary> + /// Finds the highest index of the source span that intersects <paramref name="position"/>. This means + /// that if the position is on a seam, the span to the "right" of the seam will be returned, and if + /// there is a sequence of empty spans at the position, the index of the successor of the last of them will be + /// returned. + /// </summary> + internal int FindHighestSpanIndexOfPosition(int position) + { + int lo = 0; + int hi = this.sourceSpans.Count - 1; + while (lo <= hi) + { + int mid = (lo + hi) / 2; + if (position < this.cumulativeLengths[mid]) + { + hi = mid - 1; + } + else if (position >= this.cumulativeLengths[mid + 1]) + { + lo = mid + 1; + } + else + { + // sourceSpans[mid] contains position + return mid; + } + } + Debug.Assert(position == this.Length); + return this.sourceSpans.Count - 1; + } + + /// <summary> + /// Finds the lowest index of the source span that intersects <paramref name="position"/>. This means + /// that if the position is on a seam, the span to the "left" of the seam will be returned, and if + /// there is a sequence of empty spans at the position, the index of the first of them will be + /// returned. + /// </summary> + internal int FindLowestSpanIndexOfPosition(int position) + { + int lo = 0; + int hi = this.sourceSpans.Count - 1; + while (lo <= hi) + { + int mid = (lo + hi) / 2; + if (position < this.cumulativeLengths[mid] || (mid > 0 && position == this.cumulativeLengths[mid])) + { + hi = mid - 1; + } + else if (position > this.cumulativeLengths[mid + 1]) + { + lo = mid + 1; + } + else + { + // sourceSpans[mid] contains position (or it is at the end of the span) + return mid; + } + } + Debug.Assert(position == this.Length); + return this.sourceSpans.Count - 1; + } + + internal int FindLowestSpanIndexOfLineNumber(int lineNumber) + { + int lo = 0; + int hi = this.sourceSpans.Count - 1; + while (lo <= hi) + { + int mid = (lo + hi) / 2; + if (lineNumber <= this.cumulativeLineBreakCounts[mid] && mid > 0) + { + hi = mid - 1; + } + else if (lineNumber > this.cumulativeLineBreakCounts[mid + 1]) + { + lo = mid + 1; + } + else + { + // sourceSpans[mid] contains position + return mid; + } + } + Debug.Assert(lineNumber == this.LineCount - 1); + return this.sourceSpans.Count - 1; + } + + #endregion + + #region Diagnostic Support + private string LocalToString() + { + // need a non-virtual form to call from the constructor + System.Text.StringBuilder image = new System.Text.StringBuilder(); + image.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, + "Snapshot {0,10} V{1}\r\n", TextUtilities.GetTagOrContentType(this.projectionBuffer), this.version.VersionNumber); + int cumulativeLength = 0; + for (int s = 0; s < this.sourceSpans.Count; ++s) + { + SnapshotSpan sourceSpan = this.sourceSpans[s]; + image.AppendFormat(System.Globalization.CultureInfo.InvariantCulture, + "{0,12} {1,10} {2,4} {3,12} {4}\r\n", + new Span(cumulativeLength, sourceSpan.Length), + TextUtilities.GetTagOrContentType(sourceSpan.Snapshot.TextBuffer), + "V" + sourceSpan.Snapshot.Version.VersionNumber.ToString(), + sourceSpan.Span, + TextUtilities.Escape(sourceSpan.GetText())); + cumulativeLength += sourceSpan.Length; + } + return image.ToString(); + } + + public override string ToString() + { + return LocalToString(); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs b/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs new file mode 100644 index 0000000..06b0306 --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/ProjectionSpanToChangeConverter.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Text; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Text.Utilities; + + internal class ProjectionSpanToNormalizedChangeConverter + { + private INormalizedTextChangeCollection normalizedChanges; + private bool computed = false; + private int textPosition; + private ProjectionSpanDiffer differ; + private ITextSnapshot currentSnapshot; + + public ProjectionSpanToNormalizedChangeConverter(ProjectionSpanDiffer differ, + int textPosition, + ITextSnapshot currentSnapshot) + { + this.differ = differ; + this.textPosition = textPosition; + this.currentSnapshot = currentSnapshot; + } + + public INormalizedTextChangeCollection NormalizedChanges + { + get + { + if (!computed) + { + ConstructChanges(); + computed = true; + } + return this.normalizedChanges; + } + } + + #region Private helpers + + private void ConstructChanges() + { + IDifferenceCollection<SnapshotSpan> diffs = differ.GetDifferences(); + + List<TextChange> changes = new List<TextChange>(); + int pos = this.textPosition; + + // each difference generates a text change + foreach (Difference diff in diffs) + { + pos += GetMatchSize(differ.DeletedSpans, diff.Before); + TextChange change = TextChange.Create(pos, + BufferFactoryService.StringRebuilderFromSnapshotSpans(differ.DeletedSpans, diff.Left), + BufferFactoryService.StringRebuilderFromSnapshotSpans(differ.InsertedSpans, diff.Right), + this.currentSnapshot); + changes.Add(change); + pos += change.OldLength; + } + this.normalizedChanges = NormalizedTextChangeCollection.Create(changes); + } + + private static int GetMatchSize(ReadOnlyCollection<SnapshotSpan> spans, Match match) + { + int size = 0; + if (match != null) + { + Span extent = match.Left; + for (int s = extent.Start; s < extent.End; ++s) + { + size += spans[s].Length; + } + } + return size; + } + + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Projection/ProjectionUtilities.cs b/src/Text/Impl/TextModel/Projection/ProjectionUtilities.cs new file mode 100644 index 0000000..5510114 --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/ProjectionUtilities.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + internal struct ProjectionLineInfo + { + public int lineNumber; + public int start; + public int end; // excluding line break + public int lineBreakLength; + public bool startComplete; + public bool endComplete; + + //public override string ToString() + //{ + // System.Text.StringBuilder sb = new System.Text.StringBuilder("<"); + // sb.Append("line# "); + // sb.Append(lineNumber); + // sb.Append(" start "); + // sb.Append(start); + // sb.Append(startComplete ? "!" : "?"); + // sb.Append(" end "); + // sb.Append(end); + // sb.Append(endComplete ? "!" : "?"); + // sb.Append(" lbl "); + // sb.Append(lineBreakLength); + // sb.Append(">"); + // return sb.ToString(); + //} + } + + internal enum ProjectionLineCalculationState + { + /// <summary> + /// We are searching for the map node containing the requested position. + /// </summary> + Primary, + + /// <summary> + /// The primary node has been found, but it did not contain the line break signifying the + /// end of the line, so we look to the right to discover the tail of the line. + /// </summary> + Append, + + /// <summary> + /// The primary node has been found, but it did not contain the line break signifying the + /// end of the previous line, so we look to the left to discover the head of the line. + /// </summary> + Prepend, + + /// <summary> + /// The primary node has been found, but it contained neither the line break signifying the + /// end of the previous line nor the line break signifying the end of the current line, so we + /// look both to the left and right to discover the head and tail of the line. + /// </summary> + Bipend + } +} diff --git a/src/Text/Impl/TextModel/Projection/WeakEventHook.cs b/src/Text/Impl/TextModel/Projection/WeakEventHook.cs new file mode 100644 index 0000000..043892d --- /dev/null +++ b/src/Text/Impl/TextModel/Projection/WeakEventHook.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. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Projection.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + + using Microsoft.VisualStudio.Text.Implementation; + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Utilities; + using System.Collections.ObjectModel; + + /// <summary> + /// This class is intended to manage the all the references from a projected buffer (the source buffer) back to the + /// projection buffer (the target buffer) doing the projection. You can have a scenario where someone creates a projection + /// buffer and then forgets about it. Unfortunately the projection buffer is kept alive by the events it has hooked on + /// the source buffer so we get a memory leak. + /// + /// The fix here is to gather all the references into one place so that we don't keep the target buffer alive + /// simply to maintain state no one is interested in. + /// </summary> + internal class WeakEventHook + { + private readonly WeakReference<BaseProjectionBuffer> _targetBuffer; + private BaseBuffer _sourceBuffer; + + public WeakEventHook(BaseProjectionBuffer targetBuffer, BaseBuffer sourceBuffer) + { + _targetBuffer = new WeakReference<BaseProjectionBuffer>(targetBuffer); + _sourceBuffer = sourceBuffer; + + sourceBuffer.ChangedImmediate += OnSourceTextChanged; + sourceBuffer.ContentTypeChangedImmediate += OnSourceBufferContentTypeChanged; + sourceBuffer.ReadOnlyRegionsChanged += OnSourceBufferReadOnlyRegionsChanged; + } + + public BaseBuffer SourceBuffer { get { return _sourceBuffer; } } + + public BaseProjectionBuffer GetTargetBuffer() // Not a property since it has side-effects + { + BaseProjectionBuffer targetBuffer; + if (_targetBuffer.TryGetTarget(out targetBuffer)) + { + return targetBuffer; + } + + // The target buffer that was listening to events on the source buffer has died (no one was using it). + // Dead buffers tell no tales so they get to stop listening to tales as well. Unsubscribe from the + // events it hooked on the source buffer. + this.UnsubscribeFromSourceBuffer(); + return null; + } + + private void OnSourceTextChanged(object sender, TextContentChangedEventArgs e) + { + BaseProjectionBuffer targetBuffer = this.GetTargetBuffer(); + if (targetBuffer != null) + { + targetBuffer.OnSourceTextChanged(sender, e); + } + } + + private void OnSourceBufferContentTypeChanged(object sender, ContentTypeChangedEventArgs e) + { + BaseProjectionBuffer targetBuffer = this.GetTargetBuffer(); + if (targetBuffer != null) + { + targetBuffer.OnSourceBufferContentTypeChanged(sender, e); + } + } + + private void OnSourceBufferReadOnlyRegionsChanged(object sender, SnapshotSpanEventArgs e) + { + BaseProjectionBuffer targetBuffer = this.GetTargetBuffer(); + if (targetBuffer != null) + { + targetBuffer.OnSourceBufferReadOnlyRegionsChanged(sender, e); + } + } + + public void UnsubscribeFromSourceBuffer() + { + if (_sourceBuffer != null) + { + _sourceBuffer.ChangedImmediate -= OnSourceTextChanged; + _sourceBuffer.ContentTypeChangedImmediate -= OnSourceBufferContentTypeChanged; + _sourceBuffer.ReadOnlyRegionsChanged -= OnSourceBufferReadOnlyRegionsChanged; + + _sourceBuffer = null; + } + } + } +} diff --git a/src/Text/Impl/TextModel/ReadOnlyRegion.cs b/src/Text/Impl/TextModel/ReadOnlyRegion.cs new file mode 100644 index 0000000..27c2806 --- /dev/null +++ b/src/Text/Impl/TextModel/ReadOnlyRegion.cs @@ -0,0 +1,91 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + + /// <summary> + /// A possibly empty read only region of text. + /// </summary> + internal partial class ReadOnlyRegion : IReadOnlyRegion + { + #region Private Members + + private readonly ITrackingSpan _trackingSpan; + private readonly EdgeInsertionMode _edgeInsertionMode; + + #endregion // Private Members + + #region Constructors + + /// <summary> + /// Creates a ReadOnlyRegionHandle for the given buffer and span. + /// </summary> + /// <param name="version"> + /// The <see cref="TextVersion"/> with which this read only region is associated. + /// </param> + /// <param name="span"> + /// The span of interest. + /// </param> + /// <param name="trackingMode"> + /// Specifies the tracking behavior of the read only region. + /// </param> + /// <param name="edgeInsertionMode"> + /// Specifies if insertions should be allowed at the edges + /// </param> + /// <remarks> + /// Don't call this constructor with invalid parameters. It doesn't verify all of them. + /// </remarks> + internal ReadOnlyRegion(TextVersion version, Span span, SpanTrackingMode trackingMode, EdgeInsertionMode edgeInsertionMode, DynamicReadOnlyRegionQuery callback) + { + _edgeInsertionMode = edgeInsertionMode; + // TODO: change to simple forward tracking text span + _trackingSpan = new ForwardFidelityTrackingSpan(version, span, trackingMode); + QueryCallback = callback; + } + + #endregion + + public DynamicReadOnlyRegionQuery QueryCallback { get; private set; } + + #region Public properties + + /// <summary> + /// Span of text marked read only by this region. + /// </summary> + public ITrackingSpan Span + { + get { return _trackingSpan; } + } + + /// <summary> + /// The edge insertion behavior of this read only region. + /// </summary> + public EdgeInsertionMode EdgeInsertionMode + { + get { return _edgeInsertionMode; } + } + + #endregion + + #region Overrides + /// <summary> + /// String representation of the ReadOnlyRegion. + /// </summary> + public override string ToString() + { + Span currentTrackingSpan = _trackingSpan.GetSpan(_trackingSpan.TextBuffer.CurrentSnapshot); + + return string.Format(System.Globalization.CultureInfo.InvariantCulture, + "RO: {2}{0}..{1}{3}", currentTrackingSpan.Start, currentTrackingSpan.End, _edgeInsertionMode == EdgeInsertionMode.Deny ? "[" : "(", + _edgeInsertionMode == EdgeInsertionMode.Deny ? "]" : ")"); + } + #endregion + } +} + diff --git a/src/Text/Impl/TextModel/ReadOnlySpan.cs b/src/Text/Impl/TextModel/ReadOnlySpan.cs new file mode 100644 index 0000000..27994e6 --- /dev/null +++ b/src/Text/Impl/TextModel/ReadOnlySpan.cs @@ -0,0 +1,147 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Diagnostics; + using System.Collections.Generic; + + /// <summary> + /// A span which tracks whether or not its edges can be inserted at. + /// </summary> + internal class ReadOnlySpan : ForwardFidelityTrackingSpan + { + #region Private members + private readonly EdgeInsertionMode _startEdgeInsertionMode; + private readonly EdgeInsertionMode _endEdgeInsertionMode; + #endregion + + #region Constructors + internal ReadOnlySpan(ITextVersion version, Span span, SpanTrackingMode trackingMode, EdgeInsertionMode startEdgeInsertionMode, EdgeInsertionMode endEdgeInsertionMode) + : base(version, span, trackingMode) + { + _startEdgeInsertionMode = startEdgeInsertionMode; + _endEdgeInsertionMode = endEdgeInsertionMode; + } + + internal ReadOnlySpan(ITextVersion version, IReadOnlyRegion readOnlyRegion) + : base(version, readOnlyRegion.Span.GetSpan(version), readOnlyRegion.Span.TrackingMode) + { + _startEdgeInsertionMode = readOnlyRegion.EdgeInsertionMode; + _endEdgeInsertionMode = readOnlyRegion.EdgeInsertionMode; + } + #endregion + + #region Public properties + + /// <summary> + /// Whether this span allows insertions on the start edge + /// </summary> + public EdgeInsertionMode StartEdgeInsertionMode + { + get { return _startEdgeInsertionMode; } + } + + /// <summary> + /// Whether this span allows insertions on the end edge + /// </summary> + public EdgeInsertionMode EndEdgeInsertionMode + { + get { return _endEdgeInsertionMode; } + } + + #endregion + + #region Public Methods + + /// <summary> + /// Determine if a replace of a particular span will be allowed by this read only region. + /// + /// Validate parameters before calling this method. + /// + /// Zero length read only regions only disallow inserts. + /// </summary> + /// <param name="span">The span to check to see if a change would be allowed.</param> + /// <param name="textSnapshot">The snapshot to check to see if replace is allowed.</param> + /// <returns>Whether or not the change is allowed.</returns> + public bool IsReplaceAllowed(Span span, ITextSnapshot textSnapshot) + { + // Check to see if insert is allowed since we are doing a replace of a zero length span. + if (span.Length == 0) + { + return IsInsertAllowed(span.Start, textSnapshot); + } + + Span currentSpan = this.GetSpan(textSnapshot); + + // Zero length read only regions only + // disallow inserts. + if (currentSpan.Length == 0) + { + return true; + } + + // If the span overlaps this read only + // region, then the change will not be allowed. + if ((currentSpan == span) || currentSpan.OverlapsWith(span)) + { + return false; + } + + // Nothing disallows this change + return true; + } + + /// <summary> + /// Determine if an insert is allowed by this read only region. + /// + /// Validate parameters before calling this method. + /// </summary> + /// <param name="position">The position to check if the insert is allowed.</param> + /// <param name="textSnapshot">The text snapshot to check the position relative to.</param> + /// <returns></returns> + public bool IsInsertAllowed(int position, ITextSnapshot textSnapshot) + { + Span currentSpan = this.GetSpan(textSnapshot); + + // Does this position fall in the middle of this span? + if ((currentSpan.Start < position) && (currentSpan.End > position)) + { + return false; + } + + // If edge insertions are prohibited on the start edge + // and an insert is occurring on the start edge, the insert + // is not allowed. + if (this.StartEdgeInsertionMode == EdgeInsertionMode.Deny) + { + if (position == currentSpan.Start) + { + return false; + } + } + + // If edge insertions are prohibited on the end edge + // and an insert is occurring on the end edge, the change + // is not allowed. + if (EndEdgeInsertionMode == EdgeInsertionMode.Deny) + { + if (position == currentSpan.End) + { + return false; + } + } + + // Nothing disallows this change + return true; + } + + #endregion + } +} + diff --git a/src/Text/Impl/TextModel/ReadOnlySpanCollection.cs b/src/Text/Impl/TextModel/ReadOnlySpanCollection.cs new file mode 100644 index 0000000..2bca68d --- /dev/null +++ b/src/Text/Impl/TextModel/ReadOnlySpanCollection.cs @@ -0,0 +1,385 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Collections.ObjectModel; + using System.Diagnostics; + using Microsoft.VisualStudio.Text.Utilities; + + /// <summary> + /// A collection of read only spans that are sorted by start position, with adjacent and overlapping spans combined. + /// </summary> + /// <remarks> + /// <para> + /// If two snapshots have the same read only regions, then they will have the same read only span collection. + /// </para> + /// <para> + /// Asking a ReadOnlySpanCollection if it intersects an ITrackingSpan or ITrackingPoint with a snapshot before + /// the first one the ReadOnlySpanCollection was created with is undefined. + /// </para> + /// </remarks> + internal class ReadOnlySpanCollection : ReadOnlyCollection<ReadOnlySpan> + { + readonly List<IReadOnlyRegion> regionsWithActions; + + internal IEnumerable<ReadOnlySpan> QueryAllEffectiveReadOnlySpans(ITextVersion version) + { + foreach (var span in this) + yield return span; + + foreach (var regionWithAction in this.regionsWithActions) + if (regionWithAction.QueryCallback(isEdit: false)) + yield return new ReadOnlySpan(version, regionWithAction); + } + + /// <summary> + /// Construct a ReadOnlySpanCollection that contains everything in a list of spans. + /// </summary> + /// <param name="regions">ReadOnlyRegions</param> + /// <param name="version">The version this span collection applies to.</param> + /// <remarks> + /// <para>The list of spans will be sorted and normalized (overlapping and adjoining spans will be combined).</para> + /// </remarks> + /// <exception cref="ArgumentNullException"><paramref name="regions"/> is null.</exception> + internal ReadOnlySpanCollection(TextVersion version, IEnumerable<IReadOnlyRegion> regions) + : base(NormalizeSpans(version, regions)) + { + regionsWithActions = regions.Where(region => region.QueryCallback != null).ToList(); + } + + internal bool IsReadOnly(int position, ITextSnapshot textSnapshot, bool notify) + { + foreach (var region in this.regionsWithActions) + { + if (!IsEditAllowed(region, position, textSnapshot)) + { + if (region.QueryCallback(notify)) + return true; + } + } + + // O(n) implementation which is much more straightforward + for (int i = 0; i < this.Count; i++) + { + if (!this[i].IsInsertAllowed(position, textSnapshot)) + { + return true; + } + } + + return false; + + // This is the O(lg n) implementation + #region Binary search implementation + /* + if (this.Count == 0) + { + return false; + } + + // We know that the spans don't overlap, so we can do a binary search here. + int start = this.Count / 2; + ReadOnlySpan startSpan = this[start]; + while (true) + { + Span rawSpan = startSpan.GetSpan(textSnapshot); + int newStart = start; + if (rawSpan.End < position) + { + newStart = start + 1 + start / 2; + } + else if (rawSpan.Start > position) + { + newStart = start / 2; + } + else + { + break; + } + + if (newStart == start) + { + Debug.Assert(!startSpan.GetSpan(textSnapshot).Contains(position)); + return false; + } + + if (newStart < this.Count) + { + start = newStart; + startSpan = this[start]; + } + else + { + break; + } + } + + if (!startSpan.IsInsertAllowed(position, textSnapshot)) + { + return true; + } + + if (start > 0) + { + if (!this[start - 1].IsInsertAllowed(position, textSnapshot)) + { + return true; + } + } + + if (start < this.Count - 1) + { + return !this[start + 1].IsInsertAllowed(position, textSnapshot); + } + + return false; + * */ + #endregion + } + + private static bool IsEditAllowed(IReadOnlyRegion region, int position, ITextSnapshot textSnapshot) + { + return new ReadOnlySpan(textSnapshot.Version, region).IsInsertAllowed(position, textSnapshot); + } + + private static bool IsEditAllowed(IReadOnlyRegion region, Span span, ITextSnapshot textSnapshot) + { + return new ReadOnlySpan(textSnapshot.Version, region).IsReplaceAllowed(span, textSnapshot); + } + + internal bool IsReadOnly(Span span, ITextSnapshot textSnapshot, bool notify) + { + foreach (var region in this.regionsWithActions) + { + if (!IsEditAllowed(region, span, textSnapshot)) + { + if (region.QueryCallback(notify)) + return true; + } + } + + // O(n) implementation which is much more straightforward + for (int i = 0; i < this.Count; i++) + { + if (!this[i].IsReplaceAllowed(span, textSnapshot)) + { + return true; + } + } + + return false; + + // This is the O(lg n) implementation + #region Binary search implementation + + /* + if (this.Count == 0) + { + return false; + } + + // We know that the spans don't overlap, so we can do a binary search here. + int start = this.Count / 2; + ReadOnlySpan startSpan = this[start]; + while (true) + { + Span rawSpan = startSpan.GetSpan(textSnapshot); + int newStart = start; + if (rawSpan.End < span.Start) + { + newStart = start + 1 + start / 2; + } + else if (rawSpan.Start > span.End) + { + newStart = start / 2; + } + else + { + break; + } + + if (newStart == start) + { + Debug.Assert(!startSpan.GetSpan(textSnapshot).Contains(span)); + return false; + } + + if (newStart < this.Count) + { + start = newStart; + startSpan = this[start]; + } + else + { + break; + } + } + + if (!startSpan.IsReplaceAllowed(span, textSnapshot)) + { + return true; + } + + if (start > 0) + { + if (!this[start - 1].IsReplaceAllowed(span, textSnapshot)) + { + return true; + } + } + + if (start < this.Count - 1) + { + return !this[start + 1].IsReplaceAllowed(span, textSnapshot); + } + + return false; + */ + #endregion + } + + private static IList<ReadOnlySpan> NormalizeSpans(TextVersion version, IEnumerable<IReadOnlyRegion> regions) + { + List<IReadOnlyRegion> sorted = new List<IReadOnlyRegion>(regions.Where(region => region.QueryCallback == null)); + + if (sorted.Count == 0) + { + return new FrugalList<ReadOnlySpan>(); + } + else if (sorted.Count == 1) + { + return new FrugalList<ReadOnlySpan>() {new ReadOnlySpan(version, sorted[0])}; + } + else + { + sorted.Sort((s1, s2) => s1.Span.GetSpan(version).Start.CompareTo(s2.Span.GetSpan(version).Start)); + + List<ReadOnlySpan> normalized = new List<ReadOnlySpan>(sorted.Count); + + int oldStart = sorted[0].Span.GetSpan(version).Start; + int oldEnd = sorted[0].Span.GetSpan(version).End; + EdgeInsertionMode oldStartEdgeInsertionMode = sorted[0].EdgeInsertionMode; + EdgeInsertionMode oldEndEdgeInsertionMode = sorted[0].EdgeInsertionMode; + SpanTrackingMode oldSpanTrackingMode = sorted[0].Span.TrackingMode; + for (int i = 1; (i < sorted.Count); ++i) + { + int newStart = sorted[i].Span.GetSpan(version).Start; + int newEnd = sorted[i].Span.GetSpan(version).End; + + // Since the new span's start occurs after the old span's end, we can just add the old span directly. + if (oldEnd < newStart) + { + normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), oldSpanTrackingMode, oldStartEdgeInsertionMode, oldEndEdgeInsertionMode)); + oldStart = newStart; + oldEnd = newEnd; + oldStartEdgeInsertionMode = sorted[i].EdgeInsertionMode; + oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode; + oldSpanTrackingMode = sorted[i].Span.TrackingMode; + } + else + { + // The two read only regions start at the same position + if (newStart == oldStart) + { + // If one read only region denies edge insertions, combined they do as well + if (sorted[i].EdgeInsertionMode == EdgeInsertionMode.Deny) + { + oldStartEdgeInsertionMode = EdgeInsertionMode.Deny; + } + + // This is tricky. We want one span that will be inclusive tracking, and one that won't. + if (oldSpanTrackingMode != sorted[i].Span.TrackingMode) + { + // Since the read only regions cover the same exact span, the combined one will be edge inclusive tracking + if (oldEnd == newEnd) + { + oldSpanTrackingMode = SpanTrackingMode.EdgeInclusive; + } + else if (oldEnd < newEnd) + { + // Since the old span and new span don't have the same span tracking mode and don't end in the same position, we need to create a new span that is edge inclusive + // and deny inserts between it and the next span. + normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), SpanTrackingMode.EdgeInclusive, oldStartEdgeInsertionMode, EdgeInsertionMode.Deny)); + oldStart = oldEnd; // Explicitly use the old end here since we want these spans to be adjacent + oldEnd = newEnd; + oldStartEdgeInsertionMode = sorted[i].EdgeInsertionMode; + oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode; + oldSpanTrackingMode = sorted[i].Span.TrackingMode; + } + else + { + // Since the new span ends first, create a span that is edge inclusive tracking that ends at the the new span's end. + normalized.Add(new ReadOnlySpan(version, new Span(newStart, newEnd - newStart), SpanTrackingMode.EdgeInclusive, oldStartEdgeInsertionMode, EdgeInsertionMode.Deny)); + oldStart = newEnd; // Explicitly use the new end here since we want these spans to be adjacent + } + } + } + + if (oldEnd < newEnd) + { + // If the tracking modes are different then we need to create a new span + // with the old tracking mode, and start a new span with the new span tracking mode. + // Also, if the old end and the new start are identical and both edge insertion mode's + // are allow, then we need to create a new span. + if (((oldEnd == newStart) + && + ((oldEndEdgeInsertionMode == EdgeInsertionMode.Allow) && (sorted[i].EdgeInsertionMode == EdgeInsertionMode.Allow))) + || + (oldSpanTrackingMode != sorted[i].Span.TrackingMode)) + { + normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), oldSpanTrackingMode, oldStartEdgeInsertionMode, oldEndEdgeInsertionMode)); + oldStart = oldEnd; // Explicitly use the old end here since we want these spans to be adjacent. + oldEnd = newEnd; + + // If we are splitting up the spans because of a change in tracking mode, then explicitly deny inserting between them + if (oldSpanTrackingMode != sorted[i].Span.TrackingMode) + { + oldStartEdgeInsertionMode = EdgeInsertionMode.Deny; // Explicitly use deny here since we don't want to allow insertions between these spans + } + else + { + oldStartEdgeInsertionMode = EdgeInsertionMode.Allow; + } + oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode; + oldSpanTrackingMode = sorted[i].Span.TrackingMode; + } + else + { + oldEnd = newEnd; + oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode; + } + } + else if (oldEnd == newEnd) + { + if (sorted[i].EdgeInsertionMode == EdgeInsertionMode.Deny) + { + oldEndEdgeInsertionMode = EdgeInsertionMode.Deny; + } + if (oldSpanTrackingMode != sorted[i].Span.TrackingMode) + { + normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), oldSpanTrackingMode, oldStartEdgeInsertionMode, oldEndEdgeInsertionMode)); + oldStart = newEnd; + oldEnd = newEnd; + oldStartEdgeInsertionMode = sorted[i].EdgeInsertionMode; + oldEndEdgeInsertionMode = sorted[i].EdgeInsertionMode; + oldSpanTrackingMode = sorted[i].Span.TrackingMode; + } + } + } + } + normalized.Add(new ReadOnlySpan(version, new Span(oldStart, oldEnd - oldStart), oldSpanTrackingMode, oldStartEdgeInsertionMode, oldEndEdgeInsertionMode)); + + return normalized; + } + } + } +} diff --git a/src/Text/Impl/TextModel/Storage/CharStream.cs b/src/Text/Impl/TextModel/Storage/CharStream.cs new file mode 100644 index 0000000..2179e9d --- /dev/null +++ b/src/Text/Impl/TextModel/Storage/CharStream.cs @@ -0,0 +1,133 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + /// <summary> + /// Stream that converts between characters and bytes. + /// </summary> + internal class CharStream : Stream + { + char[] data; + int length; // *byte* length of data + int position = 0; // *byte* offset into data + byte? pendingByte; + + public CharStream(char[] data, int length) + { + this.data = data; + this.length = 2 * length; + } + + public override bool CanRead { get { return true; } } + public override bool CanSeek { get { return false; } } + public override bool CanWrite { get { return true; } } + + public override void Flush() + { + } + + public override long Length { get { return (int)this.length; } } + + public override long Position + { + get { return this.position; } + set { throw new NotSupportedException(); } + } + + public override int Read(byte[] buffer, int offset, int count) + { + int actualCount = Math.Min(count, this.length - this.position); + if (actualCount > 0) + { + int residue = actualCount; + byte hi, lo; + if (this.position % 2 == 1) + { + Split(this.data[this.position / 2], out hi, out lo); + buffer[offset++] = hi; + this.position++; + residue--; + } + for (int i = 0; i < residue / 2; ++i) + { + Split(this.data[this.position / 2], out hi, out lo); + buffer[offset++] = hi; + buffer[offset++] = lo; + this.position += 2; + } + if (residue % 2 == 1) + { + Split(this.data[this.position / 2], out hi, out lo); + buffer[offset++] = lo; + this.position++; + } + } + return actualCount; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (count == 0) + { + return; + } + + if (this.pendingByte.HasValue) + { + // finish previous character + Debug.Assert(this.position % 2 == 1); + this.data[this.position / 2] = Make(this.pendingByte.Value, buffer[offset]); + this.position++; + offset++; + count--; + } + + for (int i = 0; i < count / 2; ++i) + { + this.data[this.position / 2] = Make(buffer[offset], buffer[offset + 1]); + this.position += 2; + offset += 2; + } + + if (count % 2 == 0) + { + this.pendingByte = null; + } + else + { + this.pendingByte = buffer[offset]; + this.position++; + } + } + + private char Make(byte hi, byte lo) + { + return (char)((hi << 8) | lo); + } + + private void Split(char c, out byte hi, out byte lo) + { + hi = (byte)(c >> 8); + lo = (byte)(c & 255); + } + } +} diff --git a/src/Text/Impl/TextModel/Storage/Compressor.cs b/src/Text/Impl/TextModel/Storage/Compressor.cs new file mode 100644 index 0000000..43a65c1 --- /dev/null +++ b/src/Text/Impl/TextModel/Storage/Compressor.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal static class Compressor + { +#if COMPRESSOR_TIMING + static int compressCount; + static int decompressCount; + static long compressMilliseconds; + static long decompressMilliseconds; + static long averageCompressMilliseconds; + static long averageDecompressMilliseconds; +#endif + public static byte[] Compress(char[] buffer, int length) + { + byte[] result; +#if COMPRESSOR_TIMING + Stopwatch watch = new Stopwatch(); + watch.Start(); +#endif + using (var inflatedBytes = new CharStream(buffer, length)) + { + using (var deflatedBytes = new MemoryStream(length / 9)) // guess size of compressed text + { + using (DeflateStream compress = new DeflateStream(deflatedBytes, CompressionMode.Compress)) + { + inflatedBytes.CopyTo(compress); + } + result = deflatedBytes.GetBuffer(); + } + } + +#if COMPRESSOR_TIMING + compressCount++; + compressMilliseconds += watch.ElapsedMilliseconds; + averageCompressMilliseconds = compressMilliseconds / compressCount; +#endif + return result; + } + + public static void Decompress(byte[] compressed, int length, char[] decompressed) + { +#if COMPRESSOR_TIMING + Stopwatch watch = new Stopwatch(); + watch.Start(); +#endif + + using (var deflatedBytes = new MemoryStream(compressed)) + { + using (var inflatedChars = new CharStream(decompressed, length)) + { + using (DeflateStream decompress = new DeflateStream(deflatedBytes, CompressionMode.Decompress)) + { + decompress.CopyTo(inflatedChars); + } + } + } + +#if COMPRESSOR_TIMING + decompressCount++; + decompressMilliseconds += watch.ElapsedMilliseconds; + averageDecompressMilliseconds = decompressMilliseconds / decompressCount; +#endif + } + } +} diff --git a/src/Text/Impl/TextModel/Storage/ILineBreaks.cs b/src/Text/Impl/TextModel/Storage/ILineBreaks.cs new file mode 100644 index 0000000..ccf1358 --- /dev/null +++ b/src/Text/Impl/TextModel/Storage/ILineBreaks.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + /// <summary> + /// Information about the line breaks contained in an <see cref="StringRebuilderForChars"/>. + /// </summary> + public interface ILineBreaks + { + /// <summary> + /// The number of line breaks in the <see cref="StringRebuilderForChars"/>. + /// </summary> + int Length { get; } + + /// <summary> + /// The starting position of the <paramref name="index"/>th line break. + /// </summary> + int StartOfLineBreak(int index); + + /// <summary> + /// The starting position of the <paramref name="index"/>th line break. + /// </summary> + int EndOfLineBreak(int index); + } + + public interface ILineBreaksEditor : ILineBreaks + { + /// <summary> + /// Add a line break at <paramref name="start"/> with <paramref name="length"/> + /// </summary> + void Add(int start, int length); + } +} diff --git a/src/Text/Impl/TextModel/Storage/LineBreakManager.cs b/src/Text/Impl/TextModel/Storage/LineBreakManager.cs new file mode 100644 index 0000000..2d3071b --- /dev/null +++ b/src/Text/Impl/TextModel/Storage/LineBreakManager.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + public static class LineBreakManager + { + public readonly static ILineBreaks Empty = new ShortLineBreaksEditor(0); + + public static ILineBreaksEditor CreateLineBreakEditor(int maxLength, int initialCapacity = 0) + { + return (maxLength < short.MaxValue) + ? (ILineBreaksEditor)(new ShortLineBreaksEditor(initialCapacity)) + : (ILineBreaksEditor)(new IntLineBreaksEditor(initialCapacity)); + } + + public static ILineBreaks CreateLineBreaks(string source) + { + ILineBreaksEditor lineBreaks = null; + + int index = 0; + while (index < source.Length) + { + int breakLength = TextUtilities.LengthOfLineBreak(source, index, source.Length); + if (breakLength == 0) + { + ++index; + } + else + { + if (lineBreaks == null) + lineBreaks = LineBreakManager.CreateLineBreakEditor(source.Length); + + lineBreaks.Add(index, breakLength); + index += breakLength; + } + } + + return lineBreaks ?? Empty; + } + + private class ShortLineBreaksEditor : ILineBreaksEditor + { + private const ushort MaskForPosition = 0x7fff; + private const ushort MaskForLength = 0x8000; + + private readonly static List<ushort> Empty = new List<ushort>(0); + private List<ushort> _lineBreaks = Empty; + + public ShortLineBreaksEditor(int initialCapacity) + { + if (initialCapacity > 0) + _lineBreaks = new List<ushort>(initialCapacity); + } + + public int Length => _lineBreaks.Count; + + public int LengthOfLineBreak(int index) + { + return ((_lineBreaks[index] & MaskForLength) != 0 ? 2 : 1); + } + + public int StartOfLineBreak(int index) + { + return (int)(_lineBreaks[index] & MaskForPosition); + } + public int EndOfLineBreak(int index) + { + int lineBreak = _lineBreaks[index]; + return (lineBreak & MaskForPosition) + + (((lineBreak & MaskForLength) != 0) ? 2 : 1); + } + + public void Add(int start, int length) + { + if ((start < 0) || (start > short.MaxValue)) + throw new ArgumentOutOfRangeException(nameof(start)); + if ((length < 1) || (length > 2)) + throw new ArgumentOutOfRangeException(nameof(length)); + + if (_lineBreaks == Empty) + _lineBreaks = new List<ushort>(); + + if (length == 1) + _lineBreaks.Add((ushort)start); + else if (length == 2) + _lineBreaks.Add((ushort)(start | MaskForLength)); + } + } + + private class IntLineBreaksEditor : ILineBreaksEditor + { + private const uint MaskForPosition = 0x7fffffff; + private const uint MaskForLength = 0x80000000; + + private readonly static List<uint> Empty = new List<uint>(0); + private List<uint> _lineBreaks = Empty; + + public IntLineBreaksEditor(int initialCapacity) + { + if (initialCapacity > 0) + _lineBreaks = new List<uint>(initialCapacity); + } + + public int Length => _lineBreaks.Count; + + public int LengthOfLineBreak(int index) + { + return (_lineBreaks[index] & MaskForLength) != 0 ? 2 : 1; + } + + public int StartOfLineBreak(int index) + { + return (int)(_lineBreaks[index] & MaskForPosition); + } + + public int EndOfLineBreak(int index) + { + uint lineBreak = _lineBreaks[index]; + return (int)((lineBreak & MaskForPosition) + + (((lineBreak & MaskForLength) != 0) ? 2 : 1)); + } + + public void Add(int start, int length) + { + if ((start < 0) || (start > int.MaxValue)) + throw new ArgumentOutOfRangeException(nameof(start)); + if ((length < 1) || (length > 2)) + throw new ArgumentOutOfRangeException(nameof(length)); + + if (_lineBreaks == Empty) + _lineBreaks = new List<uint>(); + + if (length == 1) + _lineBreaks.Add((uint)start); + else if (length == 2) + _lineBreaks.Add((uint)(start | MaskForLength)); + } + } + } +} diff --git a/src/Text/Impl/TextModel/Storage/Page.cs b/src/Text/Impl/TextModel/Storage/Page.cs new file mode 100644 index 0000000..5256373 --- /dev/null +++ b/src/Text/Impl/TextModel/Storage/Page.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. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Diagnostics; +namespace Microsoft.VisualStudio.Text.Implementation +{ + /// <summary> + /// Base class for a page that participates in an MRU list. + /// </summary> + internal class Page + { + private WeakReference<char[]> _uncompressedContents; + private byte[] _compressedContents; + + public readonly int Length; + public readonly PageManager Manager; + + public Page(PageManager manager, char[] contents, int length) + { + this.Manager = manager; + this.Length = length; + + _uncompressedContents = new WeakReference<char[]>(contents); + _compressedContents = Compressor.Compress(contents, length); + } + + public char[] Expand() + { + char[] contents; + if (!_uncompressedContents.TryGetTarget(out contents)) + { + contents = new char[this.Length]; + Compressor.Decompress(_compressedContents, this.Length, contents); + + _uncompressedContents.SetTarget(contents); + } + + this.Manager.UpdateMRU(this); + return contents; + + } + } +} diff --git a/src/Text/Impl/TextModel/Storage/PageManager.cs b/src/Text/Impl/TextModel/Storage/PageManager.cs new file mode 100644 index 0000000..eca1287 --- /dev/null +++ b/src/Text/Impl/TextModel/Storage/PageManager.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Threading; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal class PageManager + { + // this class inherits from page so that it participates in the MRU list, of which it is the sentinel node. + private ImmutableList<Page> _mru = ImmutableList<Page>.Empty; + private readonly int _maxPages; + + public PageManager() + { + _maxPages = TextModelOptions.CompressedStorageMaxLoadedPages; + } + + public void UpdateMRU(Page page) + { + var oldMRU = Volatile.Read(ref _mru); + while (true) + { + ImmutableList<Page> newMRU; + + int index = oldMRU.IndexOf(page); + if (index >= 0) + { + if (index == (oldMRU.Count - 1)) + { + // Page is already at the top of the MRU so nothing needs to be done. + return; + } + + // Was in the list, but not at the top. Remove it in preparation for adding it later. + newMRU = oldMRU.RemoveAt(index); + } + else if (oldMRU.Count >= _maxPages) + { + // Wasn't in the list and the list is full. Remove the oldest in preparation for adding it later. + newMRU = oldMRU.RemoveAt(0); + } + else + { + newMRU = oldMRU; + } + + newMRU = newMRU.Add(page); + + var result = Interlocked.CompareExchange(ref _mru, newMRU, oldMRU); + if (result == oldMRU) + return; + + oldMRU = result; + } + } + } +} diff --git a/src/Text/Impl/TextModel/Storage/TextImageLoader.cs b/src/Text/Impl/TextModel/Storage/TextImageLoader.cs new file mode 100644 index 0000000..428d29c --- /dev/null +++ b/src/Text/Impl/TextModel/Storage/TextImageLoader.cs @@ -0,0 +1,199 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal static class TextImageLoader + { + public const int BlockSize = 16384; + + internal static StringRebuilder Load(TextReader reader, long fileSize, string id, + out bool hasConsistentLineEndings, out int longestLineLength, + int blockSize = 0, + int minCompressedBlockSize = TextImageLoader.BlockSize) // Exposed for unit tests + { + LineEndingState lineEnding = LineEndingState.Unknown; + int currentLineLength = 0; + longestLineLength = 0; + + bool useCompressedStringRebuilders = (fileSize >= TextModelOptions.CompressedStorageFileSizeThreshold); + if (blockSize == 0) + blockSize = useCompressedStringRebuilders ? TextModelOptions.CompressedStoragePageSize : TextImageLoader.BlockSize; + + PageManager pageManager = null; + char[] buffer; + if (useCompressedStringRebuilders) + { + pageManager = new PageManager(); + buffer = new char[blockSize]; + } + else + { + buffer = TextImageLoader.AcquireBuffer(blockSize); + } + + StringRebuilder content = StringRebuilderForChars.Empty; + try + { + while (true) + { + int read = TextImageLoader.LoadNextBlock(reader, buffer); + + if (read == 0) + break; + + var lineBreaks = LineBreakManager.CreateLineBreakEditor(read); + TextImageLoader.ParseBlock(buffer, read, lineBreaks, ref lineEnding, ref currentLineLength, ref longestLineLength); + + char[] bufferForStringBuilder = buffer; + if (read < (buffer.Length / 2)) + { + // We read far less characters than buffer so copy the contents to a new buffer and reuse the original buffer. + bufferForStringBuilder = new char[read]; + Array.Copy(buffer, bufferForStringBuilder, read); + } + else + { + // We're using most of bufferForStringRebuilder so allocate a new block for the next chunk. + buffer = new char[blockSize]; + } + + var newContent = (useCompressedStringRebuilders && (read > minCompressedBlockSize)) + ? StringRebuilderForCompressedChars.Create(new Page(pageManager, bufferForStringBuilder, read), lineBreaks) + : StringRebuilderForChars.Create(bufferForStringBuilder, read, lineBreaks); + + content = content.Insert(content.Length, newContent); + } + + longestLineLength = Math.Max(longestLineLength, currentLineLength); + hasConsistentLineEndings = lineEnding != LineEndingState.Inconsistent; + } + finally + { + if (!useCompressedStringRebuilders) + { + TextImageLoader.ReleaseBuffer(buffer); + } + } + + return content; + } + + public static int LoadNextBlock(TextReader reader, char[] buffer) + { + // Reserve 1 spot for a potential CR at the end of the buffer (in which we want to add the next LF, if it exists) + int read = reader.ReadBlock(buffer, 0, buffer.Length - 1); + if ((read == buffer.Length - 1) && (buffer[read - 1] == '\r')) + { + // Last character read was a CR and there is, probably since we read the entire block, more to go. + var next = reader.Peek(); + if (next == '\n') + { + // We had a crlf that spanned the end of the buffer. Add it to the buffer and carry on. + // In theory we could append anything other than another CR but having the block end at + // the end of a line is a good thing. + reader.Read(); + buffer[read++] = '\n'; + } + } + + return read; + } + + private static void ParseBlock(char[] buffer, int length, ILineBreaksEditor lineBreaks, + ref LineEndingState lineEnding, ref int currentLineLength, ref int longestLineLength) + { + int index = 0; + while (index < length) + { + int breakLength = TextUtilities.LengthOfLineBreak(buffer, index, length); + if (breakLength == 0) + { + ++currentLineLength; + ++index; + } + else + { + lineBreaks.Add(index, breakLength); + longestLineLength = Math.Max(longestLineLength, currentLineLength); + currentLineLength = 0; + + if (lineEnding != LineEndingState.Inconsistent) + { + if (breakLength == 2) + { + if (lineEnding == LineEndingState.Unknown) + lineEnding = LineEndingState.CRLF; + else if (lineEnding != LineEndingState.CRLF) + lineEnding = LineEndingState.Inconsistent; + } + else + { + LineEndingState newLineEndingState; + switch (buffer[index]) + { + // This code needs to be kep consistent with TextUtilities.LengthOfLineBreak() + case '\r': newLineEndingState = LineEndingState.CR; break; + case '\n': newLineEndingState = LineEndingState.LF; break; + case '\u0085': newLineEndingState = LineEndingState.NEL; break; + case '\u2028': newLineEndingState = LineEndingState.LS; break; + case '\u2029': newLineEndingState = LineEndingState.PS; break; + default: throw new InvalidOperationException("Unexpected line ending"); + } + + if (lineEnding == LineEndingState.Unknown) + lineEnding = newLineEndingState; + else if (lineEnding != newLineEndingState) + lineEnding = LineEndingState.Inconsistent; + } + } + + index += breakLength; + } + } + } + + internal enum LineEndingState + { + Unknown = 0, + CRLF = 1, + CR = 2, + LF = 3, + NEL = 4, // unicode Next Line 0085 + LS = 5, // unicode Line Separator 2028 + PS = 6, // unicode Paragraph Separator 2029 + Inconsistent = 7, + } + + private static char[] pooledBuffer; + + private static char[] AcquireBuffer(int size) + { + char[] buffer = Volatile.Read(ref pooledBuffer); + if (buffer != null && buffer.Length >= size) + { + if (buffer == Interlocked.CompareExchange(ref pooledBuffer, null, buffer)) + { + return buffer; + } + } + + return new char[size]; + } + + private static void ReleaseBuffer(char[] buffer) + { + Interlocked.CompareExchange(ref pooledBuffer, buffer, null); + } + } +} diff --git a/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs new file mode 100644 index 0000000..e3e3581 --- /dev/null +++ b/src/Text/Impl/TextModel/StringRebuilder/BinaryStringRebuilder.cs @@ -0,0 +1,378 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Text; +using System.Diagnostics; +using System.Globalization; +using System.Collections; +using System.IO; +using System.Threading; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Utilities; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal sealed class BinaryStringRebuilder : StringRebuilder + { + internal readonly StringRebuilder _left; + internal readonly StringRebuilder _right; + +#if DEBUG + private static int _totalCreated = 0; + public static int TotalCreated { get { return _totalCreated; } } +#endif + + #region Private + private static StringRebuilder _crlf = StringRebuilder.Create("\r\n"); + + // internal for unit tests, a \r\n can't be spanned by left and right + internal BinaryStringRebuilder(StringRebuilder left, StringRebuilder right) + : base(left.Length + right.Length, left.LineBreakCount + right.LineBreakCount, left.FirstCharacter, right.LastCharacter) + { + Debug.Assert(left.Length > 0); + Debug.Assert(right.Length > 0); + Debug.Assert(Math.Abs(left.Depth - right.Depth) <= 1); + Debug.Assert(left.LastCharacter != '\r' || right.FirstCharacter != '\n'); + +#if DEBUG + Interlocked.Increment(ref _totalCreated); +#endif + + _left = left; + _right = right; + this.Depth = 1 + Math.Max(left.Depth, right.Depth); + } + + private static StringRebuilder ConsolidateOrBalanceTreeNode(StringRebuilder left, StringRebuilder right) + { + if ((left.Length + right.Length < TextModelOptions.StringRebuilderMaxCharactersToConsolidate) && + (left.LineBreakCount + right.LineBreakCount <= TextModelOptions.StringRebuilderMaxLinesToConsolidate)) + { + //Consolidate the two rebuilders into a single simple string rebuilder + return StringRebuilder.Consolidate(left, right); + } + else + return BinaryStringRebuilder.BalanceTreeNode(left, right); + } + + private static StringRebuilder BalanceStringRebuilder(StringRebuilder left, StringRebuilder right) + { + return BinaryStringRebuilder.BalanceTreeNode(left, right); + } + + private static StringRebuilder BalanceTreeNode(StringRebuilder left, StringRebuilder right) + { + if (left.Depth > right.Depth + 1) + return BinaryStringRebuilder.Pivot(left, right, false); + else if (right.Depth > left.Depth + 1) + return BinaryStringRebuilder.Pivot(right, left, true); + else + return new BinaryStringRebuilder(left, right); + } + + private static StringRebuilder Pivot(StringRebuilder child, StringRebuilder other, bool deepOnRightSide) + { + Debug.Assert(child.Depth > 0); //child's depth is greater than other's depth. + StringRebuilder grandchildOutside = child.Child(deepOnRightSide); + StringRebuilder grandchildInside = child.Child(!deepOnRightSide); + + if (grandchildOutside.Depth >= grandchildInside.Depth) + { + //Simple pivot. + //From this (case deepOnRightSide) + // this + // / \ + // other child + // ... / \ + // gcI gcO + // ... ... + // + //To this: + // child' + // / \ + // this' gcO + // / \ ... + // other gcI + + StringRebuilder newThis; + StringRebuilder newChild; + if (deepOnRightSide) + { + newThis = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(other, grandchildInside); + newChild = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(newThis, grandchildOutside); + } + else + { + newThis = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(grandchildInside, other); + newChild = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(grandchildOutside, newThis); + } + + return newChild; + } + else + { + //Complex pivot. + //From this (case !deepOnRightSide) + // this + // / \ + // other child + // ... / \ + // gcI gcO + // / \ ... + // ggcI ggcO + // ... ... + // + //To this: + // gcI' + // / \ + // this' child' + // / \ / \ + // other ggcI ggcO gcO + // ... ... ... ... + Debug.Assert(grandchildInside.Depth > 0); //The inside's grandchild depth is > the outside grandchild's. + StringRebuilder greatgrandchildOutside = grandchildInside.Child(deepOnRightSide); + StringRebuilder greatgrandchildInside = grandchildInside.Child(!deepOnRightSide); + + StringRebuilder newThis; + StringRebuilder newChild; + StringRebuilder newGcI; + + if (deepOnRightSide) + { + newThis = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(other, greatgrandchildInside); + newChild = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(greatgrandchildOutside, grandchildOutside); + newGcI = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(newThis, newChild); + } + else + { + newThis = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(greatgrandchildInside, other); + newChild = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(grandchildOutside, greatgrandchildOutside); + newGcI = BinaryStringRebuilder.ConsolidateOrBalanceTreeNode(newChild, newThis); + } + + return newGcI; + } + } + #endregion + + public static StringRebuilder Create(StringRebuilder left, StringRebuilder right) + { + if (left == null) + throw new ArgumentNullException("left"); + if (right == null) + throw new ArgumentNullException("right"); + + if (left.Length == 0) + return right; + else if (right.Length == 0) + return left; + else if ((left.Length + right.Length < TextModelOptions.StringRebuilderMaxCharactersToConsolidate) && + (left.LineBreakCount + right.LineBreakCount <= TextModelOptions.StringRebuilderMaxLinesToConsolidate)) + { + //Consolidate the two rebuilders into a single simple string rebuilder + return StringRebuilder.Consolidate(left, right); + } + else if ((right.FirstCharacter == '\n') && (left.LastCharacter == '\r')) + { + //Don't allow a line break to be broken across the seam + return BinaryStringRebuilder.Create(BinaryStringRebuilder.Create(left.GetSubText(new Span(0, left.Length - 1)), + _crlf), + right.GetSubText(Span.FromBounds(1, right.Length))); + } + else + { + return BinaryStringRebuilder.BalanceStringRebuilder(left, right); + } + } + + public override string ToString() + { + return string.Format(System.Globalization.CultureInfo.InvariantCulture, this.Depth % 2 == 0 ? "({0})({1})" : "[{0}][{1}]", + _left.ToString(), _right.ToString()); + } + + public override int Depth { get; } + + #region StringRebuilder Members + public override int GetLineNumberFromPosition(int position) + { + if ((position < 0) || (position > this.Length)) + throw new ArgumentOutOfRangeException("position"); + + return (position <= _left.Length) + ? _left.GetLineNumberFromPosition(position) + : (_left.LineBreakCount + + _right.GetLineNumberFromPosition(position - _left.Length)); + } + + public override void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength) + { + if ((lineNumber < 0) || (lineNumber > this.LineBreakCount)) + throw new ArgumentOutOfRangeException("lineNumber"); + + if (lineNumber < _left.LineBreakCount) + { + _left.GetLineFromLineNumber(lineNumber, out extent, out lineBreakLength); + } + else if (lineNumber > _left.LineBreakCount) + { + _right.GetLineFromLineNumber(lineNumber - _left.LineBreakCount, out extent, out lineBreakLength); + extent = new Span(extent.Start + _left.Length, extent.Length); + } + else + { + // The line crosses the seam. + int start = 0; + if (lineNumber != 0) + { + _left.GetLineFromLineNumber(lineNumber, out extent, out lineBreakLength); // ignore the returned extend.Length + + start = extent.Start; + Debug.Assert(lineBreakLength == 0); + } + + int end; + + if (lineNumber == this.LineBreakCount) + { + end = this.Length; + lineBreakLength = 0; + } + else + { + _right.GetLineFromLineNumber(0, out extent, out lineBreakLength); + end = extent.End + _left.Length; + } + + extent = Span.FromBounds(start, end); + } + } + + public override StringRebuilder GetLeaf(int position, out int offset) + { + if (position < _left.Length) + { + return _left.GetLeaf(position, out offset); + } + else + { + var leaf = _right.GetLeaf(position - _left.Length, out offset); + offset += _left.Length; + return leaf; + } + } + + public override char this[int index] + { + get + { + if ((index < 0) || (index >= this.Length)) + throw new ArgumentOutOfRangeException("index"); + + return (index < _left.Length) + ? _left[index] + : _right[index - _left.Length]; + } + } + + public override string GetText(Span span) + { + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + if (span.End <= _left.Length) + return _left.GetText(span); + else if (span.Start >= _left.Length) + return _right.GetText(new Span(span.Start - _left.Length, span.Length)); + else + { + char[] result = new char[span.Length]; + + int leftLength = _left.Length - span.Start; + _left.CopyTo(span.Start, result, 0, leftLength); + _right.CopyTo(0, result, leftLength, span.Length - leftLength); + + return new string(result); + } + } + + public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) + { + //These tests get executed a lot and are redundant: if there is an error, then the corresponding exception + //will be thrown when we reach a leaf node. + + //if (sourceIndex < 0) + // throw new ArgumentOutOfRangeException("sourceIndex"); + //if (destination == null) + // throw new ArgumentNullException("destination"); + //if (destinationIndex < 0) + // throw new ArgumentOutOfRangeException("destinationIndex"); + //if (count < 0) + // throw new ArgumentOutOfRangeException("count"); + + //if ((sourceIndex + count > this.Length) || (sourceIndex + count < 0)) + // throw new ArgumentOutOfRangeException("count"); + + //if ((destinationIndex + count > destination.Length) || (destinationIndex + count < 0)) + // throw new ArgumentOutOfRangeException("count"); + + if (sourceIndex >= _left.Length) + _right.CopyTo(sourceIndex - _left.Length, destination, destinationIndex, count); + else if (sourceIndex + count <= _left.Length) + _left.CopyTo(sourceIndex, destination, destinationIndex, count); + else + { + int leftLength = _left.Length - sourceIndex; + + _left.CopyTo(sourceIndex, destination, destinationIndex, leftLength); + _right.CopyTo(0, destination, destinationIndex + leftLength, count - leftLength); + } + } + + public override void Write(TextWriter writer, Span span) + { + if (writer == null) + throw new ArgumentNullException("writer"); + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + if (span.Start >= _left.Length) + _right.Write(writer, new Span(span.Start - _left.Length, span.Length)); + else if (span.End <= _left.Length) + _left.Write(writer, span); + else + { + _left.Write(writer, Span.FromBounds(span.Start, _left.Length)); + _right.Write(writer, Span.FromBounds(0, span.End - _left.Length)); + } + } + + public override StringRebuilder GetSubText(Span span) + { + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + if (span.Length == this.Length) + return this; + else if (span.End <= _left.Length) + return _left.GetSubText(span); + else if (span.Start >= _left.Length) + return _right.GetSubText(new Span(span.Start - _left.Length, span.Length)); + else + return BinaryStringRebuilder.Create(_left.GetSubText(Span.FromBounds(span.Start, _left.Length)), + _right.GetSubText(Span.FromBounds(0, span.End - _left.Length))); + } + + public override StringRebuilder Child(bool rightSide) + { + return rightSide ? _right : _left; + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs new file mode 100644 index 0000000..1fe8b63 --- /dev/null +++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilder.cs @@ -0,0 +1,442 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + /// <summary> + /// An immutable variation on the StringBuilder class. + /// </summary> + internal abstract class StringRebuilder + { + public readonly static StringRebuilder Empty = new StringRebuilderForString(); + +#if DEBUG + protected static int _totalCharactersScanned = 0; + public static int TotalCharactersScanned { get { return _totalCharactersScanned; } } + + protected static int _totalCharactersReturned = 0; + public static int TotalCharactersReturned { get { return _totalCharactersReturned; } } + + protected static int _totalCharactersCopied = 0; + public static int TotalCharactersCopied { get { return _totalCharactersCopied; } } +#endif + + public static StringRebuilder Create(string text) + { + if (text == null) + throw new ArgumentNullException("text"); +#if DEBUG + Interlocked.Add(ref _totalCharactersScanned, text.Length); +#endif + + return (text.Length == 0) + ? StringRebuilder.Empty + : StringRebuilderForString.Create(text, text.Length, LineBreakManager.CreateLineBreaks(text)); + } + + public static StringRebuilder Create(ITextImage image) + { + if (image == null) + throw new ArgumentNullException(nameof(image)); + + var cti = image as CachingTextImage; + if (cti != null) + return cti.Builder; + + // This shouldn't happen but as a fallback, create a new string rebuilder from the text of the provided image. + return StringRebuilder.Create(image.GetText(0, image.Length)); + } + + /// <summary> + /// Consolidate two string rebuilders, taking advantage of the fact that they have already extracted the line breaks. + /// </summary> + public static StringRebuilder Consolidate(StringRebuilder left, StringRebuilder right) + { + Debug.Assert(left.Length > 0); + Debug.Assert(right.Length > 0); + + int length = left.Length + right.Length; + char[] result = new char[length]; + + left.CopyTo(0, result, 0, left.Length); + right.CopyTo(0, result, left.Length, right.Length); + + ILineBreaks lineBreaks; + if ((left.LineBreakCount == 0) && (right.LineBreakCount == 0)) + { + lineBreaks = LineBreakManager.Empty; + //_lineBreakSpan defaults to 0, 0 which is what we want + } + else + { + ILineBreaksEditor breaks = LineBreakManager.CreateLineBreakEditor(length, left.LineBreakCount + right.LineBreakCount); + + int offset = 0; + if ((result[left.Length] == '\n') && (result[left.Length - 1] == '\r')) + { + //We have a \r\n spanning the seam ... add that as a special linebreak later. + offset = 1; + } + + int leftLines = left.LineBreakCount - offset; + for (int i = 0; (i < leftLines); ++i) + { + Span extent; + int lineBreakLength; + left.GetLineFromLineNumber(i, out extent, out lineBreakLength); + breaks.Add(extent.End, lineBreakLength); + } + + if (offset == 1) + { + breaks.Add(left.Length - 1, 2); + } + + for (int i = offset; (i < right.LineBreakCount); ++i) + { + Span extent; + int lineBreakLength; + right.GetLineFromLineNumber(i, out extent, out lineBreakLength); + breaks.Add(extent.End + left.Length, lineBreakLength); + } + + lineBreaks = breaks; + } + + return StringRebuilderForChars.Create(result, length, lineBreaks); + } + + protected StringRebuilder(int length, int lineBreakCount, char first, char last) + { + this.Length = length; + this.LineBreakCount = lineBreakCount; + this.FirstCharacter = first; + this.LastCharacter = last; + } + + /// <summary> + /// Number of characters in this <see cref="StringRebuilder"/>. + /// </summary> + public readonly int Length; + + /// <summary> + /// Number of line breaks in this <see cref="StringRebuilder"/>. + /// </summary> + /// <remarks>Line breaks consist of any of '\r', '\n', 0x85, + /// or a "\r\n" pair (which is treated as a single line break).</remarks> + public int LineBreakCount; + + public virtual int Depth => 0; + + /// <summary> + /// The first character of the StringRebuilder. \0 for a zero length StringRebuilder. + /// </summary> + public readonly char FirstCharacter; + + /// <summary> + /// The last character of the StringRebuilder. \0 for a zero length StringRebuilder. + /// </summary> + public readonly char LastCharacter; + +#region Abstract methods + /// <summary> + /// Get the zero-based line number that contains <paramref name="position"/>. + /// </summary> + /// <param name="position">Position of the character for which to get the line number.</param> + /// <returns>Number of the line that contains <paramref name="position"/>.</returns> + /// <remarks> + /// Lines are bounded by line breaks and the start and end of this <see cref="StringRebuilder"/>. + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than <see cref="Length"/>.</exception> + public abstract int GetLineNumberFromPosition(int position); + + /// <summary> + /// Get the TextImageLine associated with a zero-based line number. + /// </summary> + /// <param name="lineNumber">Line number for which to get the TextImageLine.</param> + /// <remarks> + /// <para>The last "line" in the StringRebuilder has an implicit line break length of zero.</para> + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="lineNumber"/> is less than zero or greater than <see cref="LineBreakCount"/>.</exception> + public abstract void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength); + + /// <summary> + /// Get the "leaf" node of the string rebuilder that contains position. + /// </summary> + /// <param name="position">position for which to get the leaf.</param> + /// <param name="offset">number of characters to the left of the leaf.</param> + /// <returns>leaf node from the string rebuilder.</returns> + public abstract StringRebuilder GetLeaf(int position, out int offset); + + /// <summary> + /// Character at the given index. + /// </summary> + /// <param name="index">Index to get the character for.</param> + /// <returns>Character at position <paramref name="index"/>.</returns> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="index"/> is less than zero or greater than or equal to <see cref="Length"/>.</exception> + public abstract char this[int index] { get; } + + /// <summary> + /// Copy a range of text to a destination character array. + /// </summary> + /// <param name="sourceIndex"> + /// The starting index to copy from. + /// </param> + /// <param name="destination"> + /// The destination array. + /// </param> + /// <param name="destinationIndex"> + /// The index in the destination of the first position to be copied to. + /// </param> + /// <param name="count"> + /// The number of characters to copy. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="sourceIndex"/> is less than zero or greater than <see cref="Length"/>.</exception> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="count"/> is less than zero or <paramref name="sourceIndex"/> + <paramref name="count"/> is greater than <see cref="Length"/>.</exception> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="destinationIndex"/> is less than zero or <paramref name="destinationIndex"/> + <paramref name="count"/> is greater than the length of <paramref name="destination"/>.</exception> + /// <exception cref="ArgumentNullException"><paramref name="destination"/> is null.</exception> + public abstract void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count); + + /// <summary> + /// Write a substring of the contents of this <see cref="StringRebuilder"/> to a TextWriter. + /// </summary> + /// <param name="writer">TextWriter to use.</param> + /// <param name="span">Span to write.</param> + /// <exception cref="ArgumentNullException"><paramref name="writer"/> is null.</exception> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception> + public abstract void Write(TextWriter writer, Span span); + + /// <summary> + /// Create a new StringRebuilder that corresponds to a substring of this <see cref="StringRebuilder"/>. + /// </summary> + /// <param name="span">span that defines the desired substring.</param> + /// <returns>A new StringRebuilder containing the substring.</returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> is not modified.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception> + public abstract StringRebuilder GetSubText(Span span); + + /// <summary> + /// Get the string that contains all of the characters in the specified span. + /// </summary> + /// <param name="span">Span for which to get the text.</param> + /// <returns></returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> can contain millions of characters. Be careful what you + /// ask for: you might get it.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception> + public abstract string GetText(Span span); + + public abstract StringRebuilder Child(bool rightSide); +#endregion + + /// <summary> + /// Convert a range of text to a character array. + /// </summary> + /// <param name="startIndex"> + /// The starting index of the range of text. + /// </param> + /// <param name="length"> + /// The length of the text. + /// </param> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="startIndex"/> is less than zero or greater than <see cref="Length"/>.</exception> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="length"/> is less than zero or <paramref name="startIndex"/> + <paramref name="length"/> is greater than <see cref="Length"/>.</exception> + public char[] ToCharArray(int startIndex, int length) + { + if (startIndex < 0) + throw new ArgumentOutOfRangeException("startIndex"); + + if ((length < 0) || (startIndex + length > this.Length) || (startIndex + length < 0)) + throw new ArgumentOutOfRangeException("length"); + + char[] copy = new char[length]; + this.CopyTo(startIndex, copy, 0, length); + + return copy; + } + + /// <summary> + /// Create a new StringRebuilder equivalent to appending text into this <see cref="StringRebuilder"/>. + /// </summary> + /// <param name="text">Text to append.</param> + /// <returns>A new StringRebuilder containing the insertion.</returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> is not modified.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception> + public StringRebuilder Append(string text) + { + return this.Insert(this.Length, text); + } + + /// <summary> + /// Create a new StringRebuilder equivalent to appending text into this <see cref="StringRebuilder"/>. + /// </summary> + /// <param name="text">Text to append.</param> + /// <returns>A new StringRebuilder containing the insertion.</returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> is not modified.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception> + public StringRebuilder Append(StringRebuilder text) + { + return this.Insert(this.Length, text); + } + + /// <summary> + /// Create a new StringRebuilder equivalent to inserting text into this <see cref="StringRebuilder"/>. + /// </summary> + /// <param name="position">Position at which to insert.</param> + /// <param name="text">Text to insert.</param> + /// <returns>A new StringRebuilder containing the insertion.</returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> is not modified.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than <see cref="Length"/>.</exception> + /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception> + public StringRebuilder Insert(int position, string text) + { + return this.Insert(position, StringRebuilder.Create(text)); + } + + /// <summary> + /// Create a new StringRebuilder equivalent to inserting text into this <see cref="StringRebuilder"/>. + /// </summary> + /// <param name="position">Position at which to insert.</param> + /// <param name="text">Text to insert.</param> + /// <returns>A new StringRebuilder containing the insertion.</returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> is not modified.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="position"/> is less than zero or greater than <see cref="Length"/>.</exception> + /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception> + public StringRebuilder Insert(int position, StringRebuilder text) + { + if ((position < 0) || (position > this.Length)) + throw new ArgumentOutOfRangeException("position"); + if (text == null) + throw new ArgumentNullException("text"); + + return this.Assemble(Span.FromBounds(0, position), text, Span.FromBounds(position, this.Length)); + } + + /// <summary> + /// Create a new StringRebuilder equivalent to deleting text from this <see cref="StringRebuilder"/>. + /// </summary> + /// <param name="span">Span of text to delete.</param> + /// <returns>A new StringRebuilder containing the deletion.</returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> is not modified.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception> + public StringRebuilder Delete(Span span) + { + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + return this.Assemble(Span.FromBounds(0, span.Start), Span.FromBounds(span.End, this.Length)); + } + + /// <summary> + /// Create a new StringRebuilder equivalent to replacing a contiguous span of characters + /// with different text. + /// </summary> + /// <param name="span"> + /// Span of text in this <see cref="StringRebuilder"/> to replace. + /// </param> + /// <param name="text"> + /// The new text to replace the old. + /// </param> + /// <returns> + /// A new string rebuilder containing the replacement. + /// </returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> is not modified.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception> + /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception> + public StringRebuilder Replace(Span span, string text) + { + return this.Replace(span, StringRebuilder.Create(text)); + } + + /// <summary> + /// Create a new StringRebuilder equivalent to replacing a contiguous span of characters + /// with different text. + /// </summary> + /// <param name="span"> + /// Span of text in this <see cref="StringRebuilder"/> to replace. + /// </param> + /// <param name="text"> + /// The new text to replace the old. + /// </param> + /// <returns> + /// A new string rebuilder containing the replacement. + /// </returns> + /// <remarks> + /// <para>this <see cref="StringRebuilder"/> is not modified.</para> + /// <para>This operation can be performed simultaneously on multiple threads.</para> + /// </remarks> + /// <exception cref="ArgumentOutOfRangeException"><paramref name="span"/>.End is greater than <see cref="Length"/>.</exception> + /// <exception cref="ArgumentNullException"><paramref name="text"/> is null.</exception> + public StringRebuilder Replace(Span span, StringRebuilder text) + { + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + if (text == null) + throw new ArgumentNullException("text"); + + return this.Assemble(Span.FromBounds(0, span.Start), text, Span.FromBounds(span.End, this.Length)); + } + +#region Private + private StringRebuilder Assemble(Span left, Span right) + { + if (left.Length == 0) + return this.GetSubText(right); + else if (right.Length == 0) + return this.GetSubText(left); + else if (left.Length + right.Length == this.Length) + return this; + else + return BinaryStringRebuilder.Create(this.GetSubText(left), this.GetSubText(right)); + } + + private StringRebuilder Assemble(Span left, StringRebuilder text, Span right) + { + if (text.Length == 0) + return Assemble(left, right); + else if (left.Length == 0) + return (right.Length == 0) ? text : BinaryStringRebuilder.Create(text, this.GetSubText(right)); + else if (right.Length == 0) + return BinaryStringRebuilder.Create(this.GetSubText(left), text); + else if (left.Length < right.Length) + return BinaryStringRebuilder.Create(BinaryStringRebuilder.Create(this.GetSubText(left), text), + this.GetSubText(right)); + else + return BinaryStringRebuilder.Create(this.GetSubText(left), + BinaryStringRebuilder.Create(text, this.GetSubText(right))); + } +#endregion + } +} diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs new file mode 100644 index 0000000..a4f4293 --- /dev/null +++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForChars.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal class StringRebuilderForChars : UnaryStringRebuilder + { + #region Private + internal readonly char[] _content; // Contents should be treated as immutable. + + internal static StringRebuilder Create(char[] source, int length, ILineBreaks lineBreaks) + { + return StringRebuilderForChars.Create(source, lineBreaks, 0, length, 0, lineBreaks.Length); + } + + internal static StringRebuilder Create(char[] source, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int lineBreaksLength) + { + Debug.Assert(length > 0); + + if (lineBreaksLength == 0) + return new StringRebuilderForChars(source, LineBreakManager.Empty, start, length, 0, 0, source[start], source[start + length - 1]); + else + return new StringRebuilderForChars(source, lineBreaks, start, length, lineBreaksStart, lineBreaksLength, source[start], source[start + length - 1]); + } + + private StringRebuilderForChars(char[] source, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int lineBreaksLength, char first, char last) + : base(lineBreaks, start, length, lineBreaksStart, lineBreaksLength, first, last) + { + _content = source; + } + #endregion + + public override string ToString() + { + return new string(_content, _textSpanStart, this.Length); + } + + #region StringRebuilder Members + + public override char this[int index] + { + get + { + return this.GetChar(_content, index); + } + } + + public override string GetText(Span span) + { + return this.GetText(_content, span); + } + + public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) + { + this.CopyTo(_content, sourceIndex, destination, destinationIndex, count); + } + + public override void Write(TextWriter writer, Span span) + { + this.Write(_content, writer, span); + } + + public override StringRebuilder GetSubText(Span span) + { + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + if (span.Length == 0) + return StringRebuilder.Empty; + else if (span.Length == this.Length) + return this; + else + { + int firstLineNumber; + int lastLineNumber; + this.FindFirstAndLastLines(span, out firstLineNumber, out lastLineNumber); + return StringRebuilderForChars.Create(_content, _lineBreaks, span.Start + _textSpanStart, span.Length, firstLineNumber, lastLineNumber - firstLineNumber); + } + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs new file mode 100644 index 0000000..ca80b09 --- /dev/null +++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForCompressedChars.cs @@ -0,0 +1,82 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Diagnostics; +using System.IO; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal class StringRebuilderForCompressedChars : UnaryStringRebuilder + { + private readonly Page _content; + + internal static StringRebuilder Create(Page content, ILineBreaks lineBreaks) + { + return StringRebuilderForCompressedChars.Create(content, lineBreaks, 0, content.Length, 0, lineBreaks.Length); + } + + private static StringRebuilder Create(Page content, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int linebreaksLength) + { + Debug.Assert(content.Length > 0); + + var expanded = content.Expand(); + + return new StringRebuilderForCompressedChars(content, lineBreaks, start, length, lineBreaksStart, linebreaksLength, expanded[start], expanded[start + length - 1]); + } + + private StringRebuilderForCompressedChars(Page content, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int linebreaksLength, char first, char last) + : base(lineBreaks, start, length, lineBreaksStart, linebreaksLength, first, last) + { + _content = content; + } + + #region StringRebuilder Members + public override char this[int index] + { + get + { + return this.GetChar(_content.Expand(), index); + } + } + + public override string GetText(Span span) + { + return this.GetText(_content.Expand(), span); + } + + public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) + { + this.CopyTo(_content.Expand(), sourceIndex, destination, destinationIndex, count); + } + + public override void Write(TextWriter writer, Span span) + { + this.Write(_content.Expand(), writer, span); + } + + public override StringRebuilder GetSubText(Span span) + { + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + if (span.Length == this.Length) + return this; + else if (span.Length == 0) + return StringRebuilder.Empty; + else + { + int firstLineNumber; + int lastLineNumber; + this.FindFirstAndLastLines(span, out firstLineNumber, out lastLineNumber); + + return StringRebuilderForCompressedChars.Create(_content, _lineBreaks, span.Start + _textSpanStart, span.Length, firstLineNumber, lastLineNumber - firstLineNumber); + } + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs new file mode 100644 index 0000000..56e5c8a --- /dev/null +++ b/src/Text/Impl/TextModel/StringRebuilder/StringRebuilderForString.cs @@ -0,0 +1,115 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal class StringRebuilderForString : UnaryStringRebuilder + { + #region Private + internal readonly string _content; + + internal static StringRebuilder Create(string source, int length, ILineBreaks lineBreaks) + { + return StringRebuilderForString.Create(source, lineBreaks, 0, length, 0, lineBreaks.Length); + } + + internal static StringRebuilder Create(string source, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int lineBreaksLength) + { + Debug.Assert(length != 0); + + if (lineBreaksLength == 0) + return new StringRebuilderForString(source, LineBreakManager.Empty, start, length, 0, 0, source[start], source[start + length - 1]); + else + return new StringRebuilderForString(source, lineBreaks, start, length, lineBreaksStart, lineBreaksLength, source[start], source[start + length - 1]); + } + + internal StringRebuilderForString() + : base(LineBreakManager.Empty, 0, 0, 0, 0, '\0', '\0') + { + _content = String.Empty; + } + + private StringRebuilderForString(string source, ILineBreaks lineBreaks, int start, int length, int lineBreaksStart, int lineBreaksLength, char first, char last) + : base(lineBreaks, start, length, lineBreaksStart, lineBreaksLength, first, last) + { + _content = source; + } + #endregion + + public override string ToString() + { + return _content.Substring(_textSpanStart, this.Length); + } + + #region StringRebuilder Members + + public override char this[int index] + { + get + { + if ((index < 0) || (index >= this.Length)) + throw new ArgumentOutOfRangeException(nameof(index)); + + #if DEBUG + Interlocked.Increment(ref _totalCharactersReturned); + #endif + + return _content[index + _textSpanStart]; + } + } + + public override string GetText(Span span) + { + #if DEBUG + Interlocked.Add(ref _totalCharactersReturned, span.Length); + #endif + + return _content.Substring(span.Start + _textSpanStart, span.Length); + } + + public override void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) + { + #if DEBUG + Interlocked.Add(ref _totalCharactersCopied, count); + #endif + + _content.CopyTo(sourceIndex + _textSpanStart, destination, destinationIndex, count); + } + + public override void Write(TextWriter writer, Span span) + { + if (writer == null) + throw new ArgumentNullException(nameof(writer)); + + writer.Write(this.GetText(span)); + } + + public override StringRebuilder GetSubText(Span span) + { + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + if (span.Length == 0) + return StringRebuilder.Empty; + else if (span.Length == this.Length) + return this; + else + { + int firstLineNumber; + int lastLineNumber; + this.FindFirstAndLastLines(span, out firstLineNumber, out lastLineNumber); + return StringRebuilderForString.Create(_content, _lineBreaks, span.Start + _textSpanStart, span.Length, firstLineNumber, lastLineNumber - firstLineNumber); + } + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs b/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs new file mode 100644 index 0000000..3f53283 --- /dev/null +++ b/src/Text/Impl/TextModel/StringRebuilder/UnaryStringRebuilder.cs @@ -0,0 +1,186 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + internal abstract class UnaryStringRebuilder : StringRebuilder + { + internal readonly ILineBreaks _lineBreaks; + + #if DEBUG + private static int _totalCreated = 0; + public static int TotalCreated { get { return _totalCreated; } } + #endif + + protected readonly int _textSpanStart; //subspan of _storage contained in this StringRebuilderForChars + protected readonly int _lineBreakSpanStart; //subspan of _storage.LineBreaks that contains all line breaks in this StringRebuilderForChars + protected int TextSpanEnd { get { return _textSpanStart + this.Length; } } + protected int LineBreakSpanEnd { get { return _lineBreakSpanStart + this.LineBreakCount; } } + + protected UnaryStringRebuilder(ILineBreaks lineBreaks, int start, int length, int linebreaksStart, int linebreaksLength, char first, char last) + : base(length, linebreaksLength, first, last) + { + #if DEBUG + Interlocked.Increment(ref _totalCreated); + #endif + + _lineBreaks = lineBreaks; + + _textSpanStart = start; + _lineBreakSpanStart = linebreaksStart; + } + + internal void FindFirstAndLastLines(Span span, out int firstLineNumber, out int lastLineNumber) + { + firstLineNumber = this.GetLineNumberFromPosition(span.Start) + _lineBreakSpanStart; + lastLineNumber = this.GetLineNumberFromPosition(span.End) + _lineBreakSpanStart; + + //Handle the special case where the end position falls in the middle of a linebreak. + if ((lastLineNumber < this.LineBreakSpanEnd) && + (span.End > _lineBreaks.StartOfLineBreak(lastLineNumber) - _textSpanStart)) + { + ++lastLineNumber; + } + } + + #region StringRebuilder Members + public override int GetLineNumberFromPosition(int position) + { + if ((position < 0) || (position > this.Length)) + throw new ArgumentOutOfRangeException("position"); + + //Convert position to a position relative to the start of _text. + if (position == this.Length) + { + //Handle positions at the end of the span as a special case since otherwise we + //return the incorrect value if the last line break extends past the end of _textSpan. + return this.LineBreakCount; + } + + position += _textSpanStart; + + int start = _lineBreakSpanStart; + int end = this.LineBreakSpanEnd; + + while (start < end) + { + int middle = (start + end) / 2; + if (position < _lineBreaks.EndOfLineBreak(middle)) + end = middle; + else + start = middle + 1; + } + + return start - _lineBreakSpanStart; + } + + public override void GetLineFromLineNumber(int lineNumber, out Span extent, out int lineBreakLength) + { + if ((lineNumber < 0) || (lineNumber > this.LineBreakCount)) + throw new ArgumentOutOfRangeException("lineNumber"); + + int absoluteLineNumber = _lineBreakSpanStart + lineNumber; + + int start = (lineNumber == 0) + ? 0 + : (Math.Min(this.TextSpanEnd, _lineBreaks.EndOfLineBreak(absoluteLineNumber - 1)) - _textSpanStart); + + int end; + if (lineNumber < this.LineBreakCount) + { + end = Math.Max(_textSpanStart, _lineBreaks.StartOfLineBreak(absoluteLineNumber)); + lineBreakLength = Math.Min(this.TextSpanEnd, _lineBreaks.EndOfLineBreak(absoluteLineNumber)) - end; + + end -= _textSpanStart; + } + else + { + end = this.Length; + lineBreakLength = 0; + } + + extent = Span.FromBounds(start, end); + + } + + public override StringRebuilder GetLeaf(int position, out int offset) + { + offset = 0; + return this; + } + + protected char GetChar(char[] content, int index) + { + if ((index < 0) || (index >= this.Length)) + throw new ArgumentOutOfRangeException("index"); + + #if DEBUG + Interlocked.Increment(ref _totalCharactersReturned); + #endif + + return content[index + _textSpanStart]; + } + + protected string GetText(char[] content, Span span) + { + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + #if DEBUG + Interlocked.Add(ref _totalCharactersReturned, span.Length); + #endif + + return new string(content, span.Start + _textSpanStart, span.Length); + } + + protected void CopyTo(char[] content, int sourceIndex, char[] destination, int destinationIndex, int count) + { + if (sourceIndex < 0) + throw new ArgumentOutOfRangeException("sourceIndex"); + if (destination == null) + throw new ArgumentNullException("destination"); + if (destinationIndex < 0) + throw new ArgumentOutOfRangeException("destinationIndex"); + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + + if ((sourceIndex + count > this.Length) || (sourceIndex + count < 0)) + throw new ArgumentOutOfRangeException("count"); + + if ((destinationIndex + count > destination.Length) || (destinationIndex + count < 0)) + throw new ArgumentOutOfRangeException("count"); + + #if DEBUG + Interlocked.Add(ref _totalCharactersCopied, count); + #endif + + Array.Copy(content, sourceIndex + _textSpanStart, destination, destinationIndex, count); + } + + protected void Write(char[] content, TextWriter writer, Span span) + { + if (writer == null) + throw new ArgumentNullException("writer"); + if (span.End > this.Length) + throw new ArgumentOutOfRangeException("span"); + + writer.Write(content, span.Start + _textSpanStart, span.Length); + } + + public override StringRebuilder Child(bool rightSide) + { + throw new InvalidOperationException(); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/Strings.Designer.cs b/src/Text/Impl/TextModel/Strings.Designer.cs new file mode 100644 index 0000000..47ec6fd --- /dev/null +++ b/src/Text/Impl/TextModel/Strings.Designer.cs @@ -0,0 +1,270 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.17462 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Microsoft.VisualStudio.Text.Implementation { + using System; + + + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.VisualStudio.Text.Implementation.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// <summary> + /// Looks up a localized string similar to Attempted to cancel an applied edit.. + /// </summary> + internal static string CancelAppliedEdit { + get { + return ResourceManager.GetString("CancelAppliedEdit", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Attempted to continue a canceled edit.. + /// </summary> + internal static string ContinueCanceledEdit { + get { + return ResourceManager.GetString("ContinueCanceledEdit", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to File is too large to open.. + /// </summary> + internal static string FileTooLarge { + get { + return ResourceManager.GetString("FileTooLarge", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Attempted to change edit thread of TextBuffer.. + /// </summary> + internal static string InvalidBufferThreadOwnershipChange { + get { + return ResourceManager.GetString("InvalidBufferThreadOwnershipChange", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Edge inclusive projection source span must cover entire source TextBuffer.. + /// </summary> + internal static string InvalidEdgeInclusiveSourceSpan { + get { + return ResourceManager.GetString("InvalidEdgeInclusiveSourceSpan", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to EdgeNegative source span must abut start of source TextBuffer.. + /// </summary> + internal static string InvalidEdgeNegativeSourceSpan { + get { + return ResourceManager.GetString("InvalidEdgeNegativeSourceSpan", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to EdgePositive source span must abut end of source TextBuffer.. + /// </summary> + internal static string InvalidEdgePositiveSourceSpan { + get { + return ResourceManager.GetString("InvalidEdgePositiveSourceSpan", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Inconsistent length calculation in projection snapshot.. + /// </summary> + internal static string InvalidLengthCalculation { + get { + return ResourceManager.GetString("InvalidLengthCalculation", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Inconsistent line count calculation in projection snapshot.. + /// </summary> + internal static string InvalidLineCountCalculation { + get { + return ResourceManager.GetString("InvalidLineCountCalculation", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The specified IReadOnlyRegion doesn't belong to the correct TextBuffer.. + /// </summary> + internal static string InvalidReadOnlyRegion { + get { + return ResourceManager.GetString("InvalidReadOnlyRegion", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to StringRebuilder enumerator is not at a valid position.. + /// </summary> + internal static string InvalidRebuilderEnumerator { + get { + return ResourceManager.GetString("InvalidRebuilderEnumerator", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The specified ITextSnapshot doesn't belong to the correct TextBuffer.. + /// </summary> + internal static string InvalidSnapshot { + get { + return ResourceManager.GetString("InvalidSnapshot", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Attempt to map to invalid target TextBuffer.. + /// </summary> + internal static string InvalidTargetBuffer { + get { + return ResourceManager.GetString("InvalidTargetBuffer", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Attempted to edit TextBuffer on the wrong thread.. + /// </summary> + internal static string InvalidTextBufferEditThread { + get { + return ResourceManager.GetString("InvalidTextBufferEditThread", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The specified ITrackingSpan doesn't belong to the correct TextBuffer.. + /// </summary> + internal static string InvalidTrackingSpan { + get { + return ResourceManager.GetString("InvalidTrackingSpan", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to The specified TextVersion doesn't belong to the correct TextBuffer.. + /// </summary> + internal static string InvalidVersion { + get { + return ResourceManager.GetString("InvalidVersion", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Cannot map down except from the top buffer.. + /// </summary> + internal static string MapDownFromNonTopBuffer { + get { + return ResourceManager.GetString("MapDownFromNonTopBuffer", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Source Span is neither an ITrackingSpan nor a String literal. + /// </summary> + internal static string NeitherSpanNorString { + get { + return ResourceManager.GetString("NeitherSpanNorString", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Attempted to introduce overlapping source spans.. + /// </summary> + internal static string OverlappingSourceSpans { + get { + return ResourceManager.GetString("OverlappingSourceSpans", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to There are no read only regions to remove.. + /// </summary> + internal static string RemoveNoReadOnlyRegion { + get { + return ResourceManager.GetString("RemoveNoReadOnlyRegion", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Attempted to reuse an already applied edit.. + /// </summary> + internal static string ReuseAppliedEdit { + get { + return ResourceManager.GetString("ReuseAppliedEdit", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Attempted TextBuffer edit operation while another edit is in progress.. + /// </summary> + internal static string SimultaneousEdit { + get { + return ResourceManager.GetString("SimultaneousEdit", resourceCulture); + } + } + + /// <summary> + /// Looks up a localized string similar to Attempted to create cyclic ProjectionBuffer.. + /// </summary> + internal static string SourceBufferCycle { + get { + return ResourceManager.GetString("SourceBufferCycle", resourceCulture); + } + } + } +} diff --git a/src/Text/Impl/TextModel/Strings.resx b/src/Text/Impl/TextModel/Strings.resx new file mode 100644 index 0000000..c911643 --- /dev/null +++ b/src/Text/Impl/TextModel/Strings.resx @@ -0,0 +1,189 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <data name="InvalidSnapshot" xml:space="preserve"> + <value>The specified ITextSnapshot doesn't belong to the correct TextBuffer.</value> + </data> + <data name="InvalidVersion" xml:space="preserve"> + <value>The specified TextVersion doesn't belong to the correct TextBuffer.</value> + </data> + <data name="InvalidReadOnlyRegion" xml:space="preserve"> + <value>The specified IReadOnlyRegion doesn't belong to the correct TextBuffer.</value> + </data> + <data name="InvalidTrackingSpan" xml:space="preserve"> + <value>The specified ITrackingSpan doesn't belong to the correct TextBuffer.</value> + </data> + <data name="RemoveNoReadOnlyRegion" xml:space="preserve"> + <value>There are no read only regions to remove.</value> + </data> + <data name="OverlappingSourceSpans" xml:space="preserve"> + <value>Attempted to introduce overlapping source spans.</value> + </data> + <data name="SimultaneousEdit" xml:space="preserve"> + <value>Attempted TextBuffer edit operation while another edit is in progress.</value> + </data> + <data name="CancelAppliedEdit" xml:space="preserve"> + <value>Attempted to cancel an applied edit.</value> + </data> + <data name="ReuseAppliedEdit" xml:space="preserve"> + <value>Attempted to reuse an already applied edit.</value> + </data> + <data name="ContinueCanceledEdit" xml:space="preserve"> + <value>Attempted to continue a canceled edit.</value> + </data> + <data name="InvalidRebuilderEnumerator" xml:space="preserve"> + <value>StringRebuilder enumerator is not at a valid position.</value> + </data> + <data name="InvalidEdgeInclusiveSourceSpan" xml:space="preserve"> + <value>Edge inclusive projection source span must cover entire source TextBuffer.</value> + </data> + <data name="InvalidBufferThreadOwnershipChange" xml:space="preserve"> + <value>Attempted to change edit thread of TextBuffer.</value> + </data> + <data name="InvalidTextBufferEditThread" xml:space="preserve"> + <value>Attempted to edit TextBuffer on the wrong thread.</value> + </data> + <data name="InvalidEdgeNegativeSourceSpan" xml:space="preserve"> + <value>EdgeNegative source span must abut start of source TextBuffer.</value> + </data> + <data name="InvalidEdgePositiveSourceSpan" xml:space="preserve"> + <value>EdgePositive source span must abut end of source TextBuffer.</value> + </data> + <data name="NeitherSpanNorString" xml:space="preserve"> + <value>Source Span is neither an ITrackingSpan nor a String literal</value> + </data> + <data name="SourceBufferCycle" xml:space="preserve"> + <value>Attempted to create cyclic ProjectionBuffer.</value> + </data> + <data name="InvalidLengthCalculation" xml:space="preserve"> + <value>Inconsistent length calculation in projection snapshot.</value> + </data> + <data name="InvalidTargetBuffer" xml:space="preserve"> + <value>Attempt to map to invalid target TextBuffer.</value> + </data> + <data name="MapDownFromNonTopBuffer" xml:space="preserve"> + <value>Cannot map down except from the top buffer.</value> + </data> + <data name="InvalidLineCountCalculation" xml:space="preserve"> + <value>Inconsistent line count calculation in projection snapshot.</value> + </data> + <data name="FileTooLarge" xml:space="preserve"> + <value>File is too large to open.</value> + </data> +</root>
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/TextBuffer.cs b/src/Text/Impl/TextModel/TextBuffer.cs new file mode 100644 index 0000000..b6023fa --- /dev/null +++ b/src/Text/Impl/TextModel/TextBuffer.cs @@ -0,0 +1,287 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Utilities; + + internal sealed partial class TextBuffer : BaseBuffer + { + #region BasicEdit class + private class BasicEdit : Edit, ISubordinateTextEdit + { + private TextBuffer textBuffer; + private bool subordinate; + + public BasicEdit(TextBuffer textBuffer, ITextSnapshot originSnapshot, EditOptions options, int? reiteratedVersionNumber, object editTag) + : base(textBuffer, originSnapshot, options, reiteratedVersionNumber, editTag) + { + this.textBuffer = textBuffer; + this.subordinate = true; + } + + public ITextBuffer TextBuffer + { + get { return this.textBuffer; } + } + + // this is the master edit path -- initiated from outside + protected override ITextSnapshot PerformApply() + { + CheckActive(); + this.applied = true; + this.subordinate = false; + + ITextSnapshot result = this.textBuffer.CurrentSnapshot; + + if (this.changes.Count > 0) + { + if (this.textBuffer.group.Members.Count == 1 || this.textBuffer.spurnGroup) + { + // Take a simpler faster path if there are no other buffers in our group + if (this.CheckForCancellation(() => { })) + { + FinalApply(); + result = this.textBuffer.CurrentSnapshot; + } + else + { + Debug.Assert(this.canceled); + Debug.Assert(!this.baseBuffer.editInProgress); + } + } + else + { + this.textBuffer.group.PerformMasterEdit(this.textBuffer, this, this.options, this.editTag); + + if (!this.Canceled) + { + result = this.textBuffer.CurrentSnapshot; + } + } + } + else + { + // vacuous edit + this.baseBuffer.editInProgress = false; + } + + Debug.Assert(!this.baseBuffer.editInProgress); + + return result; + } + + public void PreApply() + { + // called for all non-vacuous edits + // everything happens in FinalApply() + } + + public void FinalApply() + { + Debug.Assert(!this.canceled); + if (this.changes.Count > 0) + { + ITextEventRaiser eventRaiser = this.textBuffer.ApplyChangesAndSetSnapshot(this.changes, this.options, this.reiteratedVersionNumber, this.editTag); + this.baseBuffer.group.EnqueueEvents(eventRaiser, this.baseBuffer); + + // raise immediate events + eventRaiser.RaiseEvent(this.baseBuffer, true); + } + + this.baseBuffer.editInProgress = false; + if (this.subordinate) + { + this.baseBuffer.group.FinishEdit(); + } + } + } + #endregion + + #region ReloadEdit class + private class ReloadEdit : TextBufferBaseEdit, ISubordinateTextEdit + { + private StringRebuilder newContent; + private TextBuffer textBuffer; + private ITextSnapshot originSnapshot; + private object editTag; + private EditOptions editOptions; + private TextContentChangingEventArgs raisedChangingEventArgs; + private Action cancelAction; + + public ReloadEdit(TextBuffer textBuffer, ITextSnapshot originSnapshot, EditOptions editOptions, object editTag) : base(textBuffer) + { + this.textBuffer = textBuffer; + this.originSnapshot = originSnapshot; + this.editOptions = editOptions; + this.editTag = editTag; + } + + public ITextSnapshot ReloadContent(StringRebuilder newContent) + { + if (this.baseBuffer.IsReadOnlyImplementation(new Span(0, this.originSnapshot.Length), isEdit: true)) + { + this.applied = true; + this.baseBuffer.editInProgress = false; + this.baseBuffer.group.FinishEdit(); + return this.originSnapshot; + } + else + { + this.newContent = newContent; + this.baseBuffer.group.PerformMasterEdit(this.textBuffer, this, this.editOptions, this.editTag); + this.baseBuffer.group.FinishEdit(); + return this.textBuffer.CurrentSnapshot; + } + } + + public void PreApply() + { + } + + // copied from BaseBuffer.Edit. Could arrange to inherit. + public bool CheckForCancellation(Action cancelationResponse) + { + Debug.Assert(this.raisedChangingEventArgs == null, "just checking"); + if (this.raisedChangingEventArgs == null) + { + this.cancelAction = cancelationResponse; + this.raisedChangingEventArgs = new TextContentChangingEventArgs(this.originSnapshot, this.editTag, (args) => + { + this.Cancel(); + }); + this.baseBuffer.RaiseChangingEvent(this.raisedChangingEventArgs); + } + this.canceled = this.raisedChangingEventArgs.Canceled; + return !this.raisedChangingEventArgs.Canceled; + } + + public void FinalApply() + { + TextContentChangedEventArgs args = this.textBuffer.ApplyReload(this.newContent, this.editOptions, this.editTag); + TextContentChangedEventRaiser raiser = new TextContentChangedEventRaiser(this.originSnapshot, this.baseBuffer.currentSnapshot, this.editOptions, this.editTag); + this.applied = true; + this.baseBuffer.group.EnqueueEvents(raiser, this.baseBuffer); + raiser.RaiseEvent(this.baseBuffer, true); + this.baseBuffer.editInProgress = false; + } + + public ITextBuffer TextBuffer + { + get { return this.baseBuffer; } + } + + public void RecordMasterChangeOffset(int masterChangeOffset) + { + throw new InvalidOperationException("Reloads should not be getting offsets from any other change."); + } + } + #endregion + + #region State and Construction + bool spurnGroup; + + public TextBuffer(IContentType contentType, StringRebuilder content, ITextDifferencingService textDifferencingService, GuardedOperations guardedOperations) + : this(contentType, content, textDifferencingService, guardedOperations, false) + { + } + + public TextBuffer(IContentType contentType, StringRebuilder content, ITextDifferencingService textDifferencingService, GuardedOperations guardedOperations, bool spurnGroup) + : base(contentType, content.Length, textDifferencingService, guardedOperations) + { + // Parameters are validated outside + this.group = new BufferGroup(this); + this.builder = content; + this.spurnGroup = spurnGroup; + this.currentSnapshot = this.TakeSnapshot(); + } + #endregion + + #region Reload + /// <summary> + /// Replace the contents of the buffer with the contents of a different string rebuilder. + /// </summary> + /// <param name="newContent">The new contents of the buffer (presumably read from a file).</param> + /// <param name="editOptions">Options to apply to the edit. Differencing is highly likely to be selected.</param> + /// <param name="editTag">Arbitrary tag associated with the reload that will appear in event arguments.</param> + /// <returns></returns> + public ITextSnapshot ReloadContent(StringRebuilder newContent, EditOptions editOptions, object editTag) + { + using (ReloadEdit edit = new ReloadEdit(this, this.currentSnapshot, editOptions, editTag)) + { + return edit.ReloadContent(newContent); + } + } + + internal TextContentChangedEventArgs ApplyReload(StringRebuilder newContent, EditOptions editOptions, object editTag) + { + // we construct a normalized change list where the inserted text is a reference string that + // points "forward" to the next snapshot and whose deleted text is a reference string that points + // "backward" to the prior snapshot. This pins both snapshots in memory but that's better than materializing + // giant strings, and when (?) we have paging text storage, memory requirements will be minimal. + ITextSnapshot oldSnapshot = this.currentSnapshot; + StringRebuilder oldContent = BufferFactoryService.StringRebuilderFromSnapshotSpan(new SnapshotSpan(oldSnapshot, 0, oldSnapshot.Length)); + + TextChange change = TextChange.Create(oldPosition: 0, + oldText: oldContent, + newText: newContent, + currentSnapshot: oldSnapshot); + + + TextVersion newVersion = this.currentVersion.CreateNext(changes: null, newLength: newContent.Length, reiteratedVersionNumber: -1); + TextSnapshot newSnapshot = new TextSnapshot(this, newVersion, newContent); + + this.currentVersion.SetChanges(NormalizedTextChangeCollection.Create(new TextChange[] { change }, + editOptions.ComputeMinimalChange + ? (StringDifferenceOptions?)editOptions.DifferenceOptions + : null, + this.textDifferencingService, + oldSnapshot, newSnapshot)); + + + this.currentVersion = newVersion; + this.builder = newContent; + this.currentSnapshot = newSnapshot; + return new TextContentChangedEventArgs(oldSnapshot, newSnapshot, editOptions, editTag); + } + #endregion + + #region Overridden methods + public override ITextEdit CreateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) + { + return new BasicEdit(this, this.currentSnapshot, options, reiteratedVersionNumber, editTag); + } + + protected internal override ISubordinateTextEdit CreateSubordinateEdit(EditOptions options, int? reiteratedVersionNumber, object editTag) + { + return new BasicEdit(this, this.currentSnapshot, options, reiteratedVersionNumber, editTag); + } + + private ITextEventRaiser ApplyChangesAndSetSnapshot(FrugalList<TextChange> changes, EditOptions options, int? reiteratedVersionNumber, object editTag) + { + INormalizedTextChangeCollection normalizedChanges = NormalizedTextChangeCollection.Create(changes, + options.ComputeMinimalChange ? (StringDifferenceOptions?)options.DifferenceOptions : null, + this.textDifferencingService); + ITextSnapshot originSnapshot = base.CurrentSnapshot; + base.SetCurrentVersionAndSnapshot(normalizedChanges, reiteratedVersionNumber ?? -1); + + return new TextContentChangedEventRaiser(originSnapshot, this.CurrentSnapshot, options, editTag); + } + + protected override BaseSnapshot TakeSnapshot() + { + return new TextSnapshot(this, this.currentVersion, this.builder); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/TextChange.cs b/src/Text/Impl/TextModel/TextChange.cs new file mode 100644 index 0000000..972854e --- /dev/null +++ b/src/Text/Impl/TextModel/TextChange.cs @@ -0,0 +1,351 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using Microsoft.VisualStudio.Text.Utilities; + + /// <summary> + /// Describes a single contiguous atomic text change operation on the Text Buffer. + /// + /// All text changes are modeled as the replacement of parameter oldText with parameter newText + /// + /// Insertion: oldText == "" and newText != "" + /// Deletion: oldText != "" and newText == "" + /// Replace: oldText != "" and newText != "" + /// </summary> + internal partial class TextChange : ITextChange3 + { + #region Private Members + + private int _oldPosition; + private int _newPosition; + internal StringRebuilder _oldText, _newText; + private LineBreakBoundaryConditions _lineBreakBoundaryConditions; + private bool _isOpaque; + + private int? _lineCountDelta = null; + private int _masterChangeOffset = -1; + + #endregion // Private Members + + /// <summary> + /// Constructs a Text Change object. + /// </summary> + /// <param name="oldPosition"> + /// The character position in the TextBuffer at which the text change happened. + /// </param> + /// <param name="oldText"> + /// The text in the buffer that was replaced. + /// </param> + /// <param name="newText"> + /// The text that replaces the old text. + /// </param> + /// <param name="boundaryConditions"> + /// Information about neighboring line break characters. + /// </param> + public TextChange(int oldPosition, StringRebuilder oldText, StringRebuilder newText, LineBreakBoundaryConditions boundaryConditions) + { + if (oldPosition < 0) + { + throw new ArgumentOutOfRangeException("oldPosition"); + } + + _oldPosition = oldPosition; + _newPosition = oldPosition; + _oldText = oldText; + _newText = newText; + _lineBreakBoundaryConditions = boundaryConditions; + } + + internal TextChange(int oldPosition, string oldText, string newText, LineBreakBoundaryConditions boundaryConditions) + : this(oldPosition, StringRebuilder.Create(oldText), StringRebuilder.Create(newText), boundaryConditions) + { } + + public static TextChange Create(int oldPosition, string oldText, string newText, ITextSnapshot currentSnapshot) + { + return new TextChange(oldPosition, StringRebuilder.Create(oldText), StringRebuilder.Create(newText), ComputeLineBreakBoundaryConditions(currentSnapshot, oldPosition, oldText.Length)); + } + + public static TextChange Create(int oldPosition, StringRebuilder oldText, string newText, ITextSnapshot currentSnapshot) + { + return new TextChange(oldPosition, oldText, StringRebuilder.Create(newText), ComputeLineBreakBoundaryConditions(currentSnapshot, oldPosition, oldText.Length)); + } + + public static TextChange Create(int oldPosition, string oldText, StringRebuilder newText, ITextSnapshot currentSnapshot) + { + return new TextChange(oldPosition, StringRebuilder.Create(oldText), newText, ComputeLineBreakBoundaryConditions(currentSnapshot, oldPosition, oldText.Length)); + } + + public static TextChange Create(int oldPosition, StringRebuilder oldText, StringRebuilder newText, ITextSnapshot currentSnapshot) + { + return new TextChange(oldPosition, oldText, newText, ComputeLineBreakBoundaryConditions(currentSnapshot, oldPosition, oldText.Length)); + } + + #region Public Properties + + public Span OldSpan + { + get { return new Span(_oldPosition, _oldText.Length); } + } + + public Span NewSpan + { + get { return new Span(_newPosition, _newText.Length); } + } + + public int OldPosition + { + get { return _oldPosition; } + internal set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("value"); + } + _oldPosition = value; + } + } + + public int NewPosition + { + get { return _newPosition; } + internal set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException("value"); + } + _newPosition = value; + } + } + + public int Delta + { + get { return _newText.Length - _oldText.Length; } + } + + public int OldEnd + { + get { return _oldPosition + _oldText.Length; } + } + + public int NewEnd + { + get { return _newPosition + _newText.Length; } + } + + public string OldText + { + get { return _oldText.GetText(new Span(0, _oldText.Length)); } + } + + public string NewText + { + get { return _newText.GetText(new Span(0, _newText.Length)); } + } + + public int NewLength + { + get { return _newText.Length; } + } + + public int OldLength + { + get { return _oldText.Length; } + } + + public int LineCountDelta + { + get + { + // we are lazy + if (!_lineCountDelta.HasValue) + { + _lineCountDelta = TextModelUtilities.ComputeLineCountDelta(_lineBreakBoundaryConditions, _oldText, _newText); + } + return _lineCountDelta.Value; + } + } + + public bool IsOpaque + { + get { return _isOpaque; } + internal set { _isOpaque = value; } + } + #endregion // Public Properties + + #region Public Methods + public string GetOldText(Span span) + { + return _oldText.GetText(span); + } + + public string GetNewText(Span span) + { + return _newText.GetText(span); + } + + public char GetOldTextAt(int position) + { + if (position > this.OldLength) + { + throw new ArgumentOutOfRangeException(nameof(position)); + } + + return _oldText[position]; + } + + public char GetNewTextAt(int position) + { + if (position > this.NewLength) + { + throw new ArgumentOutOfRangeException(nameof(position)); + } + + return _newText[position]; + } + #endregion + + #region Internal Properties + internal LineBreakBoundaryConditions LineBreakBoundaryConditions + { + get { return _lineBreakBoundaryConditions; } + set + { + _lineBreakBoundaryConditions = value; + _lineCountDelta = null; + } + } + + internal void RecordMasterChangeOffset(int masterChangeOffset) + { + if (masterChangeOffset < 0) + throw new ArgumentOutOfRangeException("masterChangeOffset", "MasterChangeOffset should be non-negative."); + if (_masterChangeOffset != -1) + throw new InvalidOperationException("MasterChangeOffset has already been set."); + + _masterChangeOffset = masterChangeOffset; + } + + internal int MasterChangeOffset { get { return _masterChangeOffset == -1 ? 0 : _masterChangeOffset; } } + + internal static int Compare(TextChange x, TextChange y) + { + int diff = x.OldPosition - y.OldPosition; + if (diff != 0) + return diff; + else + return x.MasterChangeOffset - y.MasterChangeOffset; + } + + #endregion + + #region Private helper + private static LineBreakBoundaryConditions ComputeLineBreakBoundaryConditions(ITextSnapshot currentSnapshot, int position, int oldLength) + { + LineBreakBoundaryConditions conditions = LineBreakBoundaryConditions.None; + if (position > 0 && currentSnapshot[position - 1] == '\r') + { + conditions = LineBreakBoundaryConditions.PrecedingReturn; + } + int end = position + oldLength; + if (end < currentSnapshot.Length && currentSnapshot[end] == '\n') + { + conditions = conditions | LineBreakBoundaryConditions.SucceedingNewline; + } + return conditions; + } + #endregion + + #region Overridden methods + public string ToString(bool brief) + { + if (brief) + { + return string.Format(System.Globalization.CultureInfo.InvariantCulture, "old={0} new={1}", this.OldSpan, this.NewSpan); + } + else + { + return string.Format(System.Globalization.CultureInfo.InvariantCulture, + "old={0}:'{1}' new={2}:'{3}'", + this.OldSpan, TextUtilities.Escape(this.OldText), this.NewSpan, TextUtilities.Escape(this.NewText, 40)); + } + } + + public override string ToString() + { + return ToString(false); + } + #endregion + + public static StringRebuilder OldStringRebuilder(ITextChange change) + { + var textChange = change as TextChange; + return (textChange != null) ? textChange._oldText : StringRebuilder.Create(change.OldText); + } + + public static StringRebuilder NewStringRebuilder(ITextChange change) + { + var textChange = change as TextChange; + return (textChange != null) ? textChange._newText : StringRebuilder.Create(change.NewText); + } + + public static StringRebuilder ChangeOldSubText(ITextChange change, int start, int length) + { + var textChange = change as TextChange; + if (textChange != null) + return textChange._oldText.GetSubText(new Span(start, length)); + + var change3 = change as ITextChange3; + if (change3 != null) + return StringRebuilder.Create(change3.GetOldText(new Span(start, length))); + + return StringRebuilder.Create(change.OldText.Substring(start, length)); + } + + public static StringRebuilder ChangeNewSubText(ITextChange change, int start, int length) + { + var textChange = change as TextChange; + if (textChange != null) + return textChange._newText.GetSubText(new Span(start, length)); + + var change3 = change as ITextChange3; + if (change3 != null) + return StringRebuilder.Create(change3.GetNewText(new Span(start, length))); + + return StringRebuilder.Create(change.NewText.Substring(start, length)); + } + + public static string ChangeOldSubstring(ITextChange change, int start, int length) + { + var textChange = change as TextChange; + if (textChange != null) + return textChange._oldText.GetText(new Span(start, length)); + + var change3 = change as ITextChange3; + if (change3 != null) + return change3.GetOldText(new Span(start, length)); + + return change.OldText.Substring(start, length); + } + + public static string ChangeNewSubstring(ITextChange change, int start, int length) + { + var textChange = change as TextChange; + if (textChange != null) + return textChange._newText.GetText(new Span(start, length)); + + var change3 = change as ITextChange3; + if (change3 != null) + return change3.GetNewText(new Span(start, length)); + + return change.NewText.Substring(start, length); + } + } +} diff --git a/src/Text/Impl/TextModel/TextDocument.cs b/src/Text/Impl/TextModel/TextDocument.cs new file mode 100644 index 0000000..e546e97 --- /dev/null +++ b/src/Text/Impl/TextModel/TextDocument.cs @@ -0,0 +1,593 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Text; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text.Editor; + + internal partial class TextDocument : ITextDocument + { + #region Private Members + + private readonly TextDocumentFactoryService _textDocumentFactoryService; + private ITextBuffer _textBuffer; + private Encoding _encoding; + private string _filePath; + //If the user explicitly chooses the encoding, we want to respect their chosen encoding + private bool _explicitEncoding; + + //This corresponds to the tools option "Auto-detect UTF-8 encoding" + //Unfortunately, we cannot dynamically read the option value without adding a dependency on TextLogic which results in a layering violation. + //Therefore we're going to cache this value on creation of the text document and it will persist the lifetime of the document. + private bool _attemptUtf8Detection = true; + + private DateTime _lastSavedTimeUtc; + private DateTime _lastModifiedTimeUtc; + private int _cleanReiteratedVersion; // The ReiteratedVersionNumber at which the document was last clean (saved, opened, or created) + private bool _isDirty; + private bool _isDisposed; + private bool _raisingDirtyStateChangedEvent; + private bool _raisingFileActionChangedEvent; + private bool _reloadingFile; + + #endregion + + #region Construction + internal TextDocument(ITextBuffer textBuffer, string filePath, DateTime lastModifiedTime, TextDocumentFactoryService textDocumentFactoryService) + : this(textBuffer, filePath, lastModifiedTime, textDocumentFactoryService, Encoding.UTF8) { } + + internal TextDocument(ITextBuffer textBuffer, string filePath, DateTime lastModifiedTime, TextDocumentFactoryService textDocumentFactoryService, Encoding encoding, bool explicitEncoding = false, bool attemptUtf8Detection = true) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + if (filePath == null) + { + throw new ArgumentNullException("filePath"); + } + if (textDocumentFactoryService == null) + { + throw new ArgumentNullException("textDocumentFactoryService"); + } + if (encoding == null) + { + throw new ArgumentNullException("encoding"); + } + + _textBuffer = textBuffer; + _filePath = filePath; + _lastModifiedTimeUtc = lastModifiedTime; + _textDocumentFactoryService = textDocumentFactoryService; + _cleanReiteratedVersion = _textBuffer.CurrentSnapshot.Version.ReiteratedVersionNumber; + _isDisposed = false; + _isDirty = false; + _reloadingFile = false; + _raisingDirtyStateChangedEvent = false; + _raisingFileActionChangedEvent = false; + _encoding = encoding; + _explicitEncoding = explicitEncoding; + _attemptUtf8Detection = attemptUtf8Detection; + + // Keep track of when the text buffer has been changed so that we can update the LastContentModifiedTime + _textBuffer.ChangedHighPriority += TextBufferChangedHandler; + + _textBuffer.Properties.AddProperty(typeof(ITextDocument), this); + } + #endregion + + #region ITextDocument Members + + public string FilePath + { + get { return _filePath; } + } + + public ITextBuffer TextBuffer + { + get { return _textBuffer; } + } + + public bool IsDirty + { + get { return _isDirty; } + } + + public DateTime LastSavedTime + { + get { return _lastSavedTimeUtc; } + } + + public DateTime LastContentModifiedTime + { + get { return _lastModifiedTimeUtc; } + } + + public void Rename(string newFilePath) + { + if (_isDisposed) + { + throw new ObjectDisposedException("ITextDocument"); + } + if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent) + { + throw new InvalidOperationException(); + } + if (newFilePath == null) + { + throw new ArgumentNullException("newFilePath"); + } + + _filePath = newFilePath; + + RaiseFileActionChangedEvent(_lastModifiedTimeUtc, FileActionTypes.DocumentRenamed, _filePath); + } + + public ReloadResult Reload() + { + return Reload(EditOptions.None); + } + + private void ReloadBufferFromStream(Stream stream, long fileSize, EditOptions options, Encoding encoding) + { + using (var streamReader = new EncodedStreamReader.NonStreamClosingStreamReader(stream, encoding, detectEncodingFromByteOrderMarks: false)) + { + TextBuffer concreteBuffer = _textBuffer as TextBuffer; + if (concreteBuffer != null) + { + bool hasConsistentLineEndings; + int longestLineLength; + StringRebuilder newContent = TextImageLoader.Load(streamReader, fileSize, _filePath, out hasConsistentLineEndings, out longestLineLength); + + if (!hasConsistentLineEndings) + { + // leave a sign that line endings are inconsistent. This is rather nasty but for now + // we don't want to pollute the API with this factoid. + concreteBuffer.Properties["InconsistentLineEndings"] = true; + } + else + { + // this covers a really obscure case where on initial load the file had inconsistent line + // endings, but the UI settings were such that it was ignored, and since then the file has + // acquired consistent line endings and the UI settings have also changed. + concreteBuffer.Properties.RemoveProperty("InconsistentLineEndings"); + } + // leave a similar sign about the longest line in the buffer. + concreteBuffer.Properties["LongestLineLength"] = longestLineLength; + + concreteBuffer.ReloadContent(newContent, options, editTag: this); + } + else + { + // we may hit this path if somebody mocks the text buffer in a test. + using (var edit = _textBuffer.CreateEdit(options, null, editTag: this)) + { + if (edit.Replace(new Span(0, edit.Snapshot.Length), streamReader.ReadToEnd())) + { + edit.Apply(); + } + else + { + edit.Cancel(); + } + } + } + } + } + + public ReloadResult Reload(EditOptions options) + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(ITextDocument)); + } + if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent) + { + throw new InvalidOperationException(); + } + + Encoding newEncoding; + var beforeSnapshot = _textBuffer.CurrentSnapshot; + bool characterSubstitutionsOccurred = false; + + try + { + _reloadingFile = true; + + // Load the file and read the contents to the text buffer + long fileSize; + + using (var stream = TextDocumentFactoryService.OpenFileGuts(_filePath, out _lastModifiedTimeUtc, out fileSize)) + { + var detectors = ExtensionSelector.SelectMatchingExtensions(_textDocumentFactoryService.OrderedEncodingDetectors, _textBuffer.ContentType); + + if(_explicitEncoding) + { + // If the user explicitly chose their encoding, we want to respect it. + newEncoding = this.Encoding; + } + else + { + newEncoding = EncodedStreamReader.DetectEncoding(stream, detectors, _textDocumentFactoryService.GuardedOperations); + } + + if (newEncoding == null && _attemptUtf8Detection) + { + try + { + var detectorEncoding = new ExtendedCharacterDetector(); + + ReloadBufferFromStream(stream, fileSize, options, detectorEncoding); + + if (detectorEncoding.DecodedExtendedCharacters) + { + // Valid UTF-8 but has bytes that are not merely ASCII. + newEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + } + else + { + // Valid UTF8 but no extended characters, so it looks like valid ASCII. + // However, we don't use ASCII here because of the following scenario: + // The user with a non-English US system encoding opens a code file that happens to contain ASCII-only contents + // Therefore we'll just use their system encoding. + newEncoding = Encoding.Default; + } + } + catch (DecoderFallbackException) + { + // Not valid UTF-8. + // Proceed to the next if block to try the system's default codepage. + // For example, this occurs when you have extended characters like € in a UTF-8 file or ANSI file. + // We reset the stream so we can continue loading with the default system encoding. + Debug.Assert(newEncoding == null); + Debug.Assert(beforeSnapshot.Version.Next == null); + stream.Position = 0; + } + } + + // If all else didn't work, use system's default encoding. + if (newEncoding == null) + { + newEncoding = Encoding.Default; + } + + //If there is no "Next" version of the original snapshot, we have not successfully reloaded the document + if(beforeSnapshot.Version.Next == null) + { + //We use this fall back detector to observe whether or not character substitutions + //occur while we're reading the stream + var fallbackDetector = new FallbackDetector(newEncoding.DecoderFallback); + var modifiedEncoding = (Encoding)newEncoding.Clone(); + modifiedEncoding.DecoderFallback = fallbackDetector; + + Debug.Assert(stream.Position == 0); + ReloadBufferFromStream(stream, fileSize, options, modifiedEncoding); + + if(fallbackDetector.FallbackOccurred) + { + characterSubstitutionsOccurred = fallbackDetector.FallbackOccurred; + } + } + } + } + finally + { + _reloadingFile = false; + } + + //The snapshot on a reload will change even if the contents of the before & after files are identical (differences will simply find an + //empty set of changes) so this test is a measure of whether of not the reload succeeded. + if (beforeSnapshot.Version.Next != null) + { + // Update status + // set the "clean" reiterated version number to the reiterated version number of the version immediately + // after the before snapshot (which is the state of the buffer after loading the document but before any + // subsequent edits made in the text buffer changed events). + _cleanReiteratedVersion = beforeSnapshot.Version.Next.ReiteratedVersionNumber; + + // TODO: the following event really should be queued up through the buffer group so that it comes before + // the text changed event (and any subsequent text changed event invoked from an event handler) + RaiseFileActionChangedEvent(_lastModifiedTimeUtc, FileActionTypes.ContentLoadedFromDisk, _filePath); + this.Encoding = newEncoding; + return characterSubstitutionsOccurred ? ReloadResult.SucceededWithCharacterSubstitutions : ReloadResult.Succeeded; + } + else + { + return ReloadResult.Aborted; + } + } + + public bool IsReloading + { + get { return _reloadingFile; } + } + + public void Save() + { + if (_isDisposed) + { + throw new ObjectDisposedException("ITextDocument"); + } + if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent) + { + throw new InvalidOperationException(); + } + + // Before saving the document check if we need to change the encoding of the file as per codingconventions of the repo. + if (_textBuffer.Properties.TryGetProperty<Encoding>("EncodingToBeAppliedOnSave", out Encoding encodingToBeAppliedOnSave)) + { + this.Encoding = encodingToBeAppliedOnSave; + _textBuffer.Properties.RemoveProperty("EncodingToBeAppliedOnSave"); + } + + PerformSave(FileMode.Create, _filePath, false); + UpdateSaveStatus(_filePath, false); + } + + public void SaveAs(string filePath, bool overwrite) + { + SaveAs(filePath, overwrite, false); + } + + public void SaveAs(string filePath, bool overwrite, IContentType newContentType) + { + SaveAs(filePath, overwrite, false, newContentType); + } + + public void SaveCopy(string filePath, bool overwrite) + { + SaveCopy(filePath, overwrite, false); + } + + public void SaveAs(string filePath, bool overwrite, bool createFolder) + { + if (_isDisposed) + { + throw new ObjectDisposedException("ITextDocument"); + } + if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent) + { + throw new InvalidOperationException(); + } + if (filePath == null) + { + throw new ArgumentNullException("filePath"); + } + + PerformSave(overwrite ? FileMode.Create : FileMode.CreateNew, filePath, createFolder); + UpdateSaveStatus(filePath, _filePath != filePath); + + // file path won't be updated if the save fails (in which case PerformSave will throw an exception) + + _filePath = filePath; + } + + public void SaveAs(string filePath, bool overwrite, bool createFolder, IContentType newContentType) + { + if (newContentType == null) + { + throw new ArgumentNullException("newContentType"); + } + SaveAs(filePath, overwrite, createFolder); + // content type won't be changed if the save fails (in which case SaveAs will throw an exception) + _textBuffer.ChangeContentType(newContentType, null); + } + + public void SaveCopy(string filePath, bool overwrite, bool createFolder) + { + if (_isDisposed) + { + throw new ObjectDisposedException("ITextDocument"); + } + if (filePath == null) + { + throw new ArgumentNullException("filePath"); + } + + PerformSave(overwrite ? FileMode.Create : FileMode.CreateNew, filePath, createFolder); + // Don't update save status + } + + private void PerformSave(FileMode fileMode, string filePath, bool createFolder) + { + // check whether directory of the path exists + if (createFolder) + { + string fileDirectoryName = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(fileDirectoryName) && !Directory.Exists(fileDirectoryName)) + { + Directory.CreateDirectory(fileDirectoryName); + } + } + FileUtilities.SaveSnapshot(_textBuffer.CurrentSnapshot, fileMode, _encoding, filePath); + } + + private void UpdateSaveStatus(string filePath, bool renamed) + { + FileInfo fileInfo = new FileInfo(filePath); + _lastSavedTimeUtc = fileInfo.LastWriteTimeUtc; + _cleanReiteratedVersion = _textBuffer.CurrentSnapshot.Version.ReiteratedVersionNumber; + + FileActionTypes actionType = FileActionTypes.ContentSavedToDisk; + if (renamed) + { + actionType |= FileActionTypes.DocumentRenamed; + } + RaiseFileActionChangedEvent(_lastSavedTimeUtc, actionType, filePath); + } + + public void UpdateDirtyState(bool isDirtied, DateTime lastContentModifiedTimeUtc) + { + if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent) + { + throw new InvalidOperationException(); + } + + if (_isDisposed) + { + throw new ObjectDisposedException("ITextDocument"); + } + + _lastModifiedTimeUtc = lastContentModifiedTimeUtc; + + RaiseDirtyStateChangedEvent(isDirtied); + } + + public Encoding Encoding + { + get + { + return _encoding; + } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + Encoding oldEncoding = _encoding; + + _encoding = value; + + if (!_encoding.Equals(oldEncoding)) + { + _textDocumentFactoryService.GuardedOperations.RaiseEvent(this, EncodingChanged, new EncodingChangedEventArgs(oldEncoding, _encoding)); + } + } + } + + public void SetEncoderFallback(EncoderFallback fallback) + { + _encoding = Encoding.GetEncoding(_encoding.CodePage, fallback, _encoding.DecoderFallback); + // no event here! + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + if (_raisingDirtyStateChangedEvent || _raisingFileActionChangedEvent) + { + throw new InvalidOperationException(); + } + + if (!_isDisposed) + { + _textBuffer.ChangedHighPriority -= TextBufferChangedHandler; + _textBuffer.Properties.RemoveProperty(typeof(ITextDocument)); + + _isDisposed = true; + + _textDocumentFactoryService.RaiseTextDocumentDisposed(this); + GC.SuppressFinalize(this); + + _textBuffer = null; // why? + } + } + + #endregion + + #region Private helpers + + private void TextBufferChangedHandler(object sender, TextContentChangedEventArgs e) + { + // We don't want to process textbuffer changes that were caused by ourselves + if (e.EditTag != this) + { + _lastModifiedTimeUtc = DateTime.UtcNow; + + // If the edit was the result of an undo/redo action that took us back to the clean ReiteratedVersionNumber, + // the document is no longer dirty + if (e.AfterVersion.ReiteratedVersionNumber == _cleanReiteratedVersion) + { + RaiseDirtyStateChangedEvent(false); + } + else + { + RaiseDirtyStateChangedEvent(true); + } + } + } + + /// <summary> + /// Raises events for when the dirty state changes + /// </summary> + private void RaiseDirtyStateChangedEvent(bool newDirtyState) + { + _raisingDirtyStateChangedEvent = true; + + try + { + bool dirtyStateChanged = (_isDirty != newDirtyState); + + if (dirtyStateChanged) + { + _isDirty = newDirtyState; + + _textDocumentFactoryService.GuardedOperations.RaiseEvent(this, DirtyStateChanged); + } + } + finally + { + _raisingDirtyStateChangedEvent = false; + } + } + + /// <summary> + /// Raises events for when a file load/save occurs. + /// </summary> + private void RaiseFileActionChangedEvent(DateTime actionTime, FileActionTypes actionType, string filePath) + { + _raisingFileActionChangedEvent = true; + + try + { + if ((actionType & FileActionTypes.ContentLoadedFromDisk) == FileActionTypes.ContentLoadedFromDisk || + (actionType & FileActionTypes.ContentSavedToDisk) == FileActionTypes.ContentSavedToDisk) + { + // We did a reload or a save so we probably want to clear the dirty flag unless someone modified + // the buffer -- changing the reiterated version number -- between the reload and when this call was made + // (for example, modifying the buffer in the text buffer changed event). + if (_cleanReiteratedVersion == _textBuffer.CurrentSnapshot.Version.ReiteratedVersionNumber) + { + RaiseDirtyStateChangedEvent(false); + } + } + + _textDocumentFactoryService.GuardedOperations.RaiseEvent(this, FileActionOccurred, new TextDocumentFileActionEventArgs(filePath, actionTime, actionType)); + + } + finally + { + _raisingFileActionChangedEvent = false; + } + } + + #endregion + + public event EventHandler<TextDocumentFileActionEventArgs> FileActionOccurred; + public event EventHandler DirtyStateChanged; + public event EventHandler<EncodingChangedEventArgs> EncodingChanged; + + /// <summary> + /// An accessor for isDisposed to be used by <see cref="TextDocumentFactoryService"/>. + /// </summary> + internal bool IsDisposed + { + get { return _isDisposed; } + } + } +} diff --git a/src/Text/Impl/TextModel/TextDocumentFactoryService.cs b/src/Text/Impl/TextModel/TextDocumentFactoryService.cs new file mode 100644 index 0000000..ff59ae3 --- /dev/null +++ b/src/Text/Impl/TextModel/TextDocumentFactoryService.cs @@ -0,0 +1,340 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.Composition; + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + using System.Diagnostics; + using Microsoft.VisualStudio.Text.Editor; + + [Export(typeof(ITextDocumentFactoryService))] + internal sealed partial class TextDocumentFactoryService : ITextDocumentFactoryService + { + #region Internal Consumptions + + [Import] + internal ITextBufferFactoryService BufferFactoryService { get; set; } + + [ImportMany] + internal List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> UnorderedEncodingDetectors { get; set; } + + [Import] + internal GuardedOperations GuardedOperations { get; set; } + + #endregion + + internal static Encoding DefaultEncoding = Encoding.Default; // Exposed for unit tests. + + #region ITextDocumentFactoryService Members + + public ITextDocument CreateAndLoadTextDocument(string filePath, IContentType contentType) + { + bool unused; + return CreateAndLoadTextDocument(filePath, contentType, attemptUtf8Detection: true, characterSubstitutionsOccurred: out unused); + } + + public ITextDocument CreateAndLoadTextDocument(string filePath, IContentType contentType, Encoding encoding, out bool characterSubstitutionsOccurred) + { + if (filePath == null) + { + throw new ArgumentNullException("filePath"); + } + + if (contentType == null) + { + throw new ArgumentNullException("contentType"); + } + + if (encoding == null) + { + throw new ArgumentNullException("encoding"); + } + + var fallbackDetector = new FallbackDetector(encoding.DecoderFallback); + var modifiedEncoding = (Encoding)encoding.Clone(); + modifiedEncoding.DecoderFallback = fallbackDetector; + + ITextBuffer buffer; + DateTime lastModified; + long fileSize; + using (Stream stream = OpenFile(filePath, out lastModified, out fileSize)) + { + // Caller knows best, so don't use byte order marks. + using (StreamReader reader = new StreamReader(stream, modifiedEncoding, detectEncodingFromByteOrderMarks: false)) + { + System.Diagnostics.Debug.Assert(encoding.CodePage == reader.CurrentEncoding.CodePage); + buffer = ((ITextBufferFactoryService2)BufferFactoryService).CreateTextBuffer(reader, contentType, fileSize, filePath); + } + } + + characterSubstitutionsOccurred = fallbackDetector.FallbackOccurred; + +#if _DEBUG + TextUtilities.TagBuffer(buffer, filePath); +#endif + TextDocument textDocument = new TextDocument(buffer, filePath, lastModified, this, encoding, explicitEncoding: true); + + RaiseTextDocumentCreated(textDocument); + + return textDocument; + } + + public ITextDocument CreateAndLoadTextDocument(string filePath, IContentType contentType, bool attemptUtf8Detection, out bool characterSubstitutionsOccurred) + { + if (filePath == null) + { + throw new ArgumentNullException(nameof(filePath)); + } + + if (contentType == null) + { + throw new ArgumentNullException(nameof(contentType)); + } + + characterSubstitutionsOccurred = false; + + Encoding chosenEncoding = null; + ITextBuffer buffer = null; + DateTime lastModified; + long fileSize; + + // select matching detectors without instantiating any + var detectors = ExtensionSelector.SelectMatchingExtensions(OrderedEncodingDetectors, contentType); + + using (Stream stream = OpenFile(filePath, out lastModified, out fileSize)) + { + // First, look for a byte order marker and let the encoding detecters + // suggest encodings. + chosenEncoding = EncodedStreamReader.DetectEncoding(stream, detectors, GuardedOperations); + + // If that didn't produce a result, tentatively try to open as UTF 8. + if (chosenEncoding == null && attemptUtf8Detection) + { + try + { + var detectorEncoding = new ExtendedCharacterDetector(); + + using (StreamReader reader = new EncodedStreamReader.NonStreamClosingStreamReader(stream, detectorEncoding, false)) + { + buffer = ((ITextBufferFactoryService2)BufferFactoryService).CreateTextBuffer(reader, contentType, fileSize, filePath); + characterSubstitutionsOccurred = false; + } + + if (detectorEncoding.DecodedExtendedCharacters) + { + // Valid UTF-8 but has bytes that are not merely ASCII. + chosenEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + } + else + { + // Valid UTF8 but no extended characters, so it's valid ASCII. + // We don't use ASCII here because of the following scenario: + // The user with a non-ENU system encoding opens a code file with ASCII-only contents + chosenEncoding = DefaultEncoding; + } + } + catch (DecoderFallbackException) + { + // Not valid UTF-8. + // Proceed to the next if block to try the system's default codepage. + Debug.Assert(buffer == null); + buffer = null; + stream.Position = 0; + } + } + + Debug.Assert(buffer == null || chosenEncoding != null); + + // If all else didn't work, use system's default encoding. + if (chosenEncoding == null) + { + chosenEncoding = DefaultEncoding; + } + + if (buffer == null) + { + var fallbackDetector = new FallbackDetector(chosenEncoding.DecoderFallback); + var modifiedEncoding = (Encoding)chosenEncoding.Clone(); + modifiedEncoding.DecoderFallback = fallbackDetector; + + Debug.Assert(stream.Position == 0); + + using (StreamReader reader = new EncodedStreamReader.NonStreamClosingStreamReader(stream, modifiedEncoding, detectEncodingFromByteOrderMarks: false)) + { + Debug.Assert(chosenEncoding.CodePage == reader.CurrentEncoding.CodePage); + buffer = ((ITextBufferFactoryService2)BufferFactoryService).CreateTextBuffer(reader, contentType, fileSize, filePath); + } + + characterSubstitutionsOccurred = fallbackDetector.FallbackOccurred; + } + } + + TextDocument textDocument = new TextDocument(buffer, filePath, lastModified, this, chosenEncoding, attemptUtf8Detection: attemptUtf8Detection); + + RaiseTextDocumentCreated(textDocument); + + return textDocument; + } + + public ITextDocument CreateTextDocument(ITextBuffer textBuffer, string filePath) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + + if (filePath == null) + { + throw new ArgumentNullException("filePath"); + } + + TextDocument textDocument = new TextDocument(textBuffer, filePath, DateTime.UtcNow, this, Encoding.UTF8); + RaiseTextDocumentCreated(textDocument); + + return textDocument; + } + + public bool TryGetTextDocument(ITextBuffer textBuffer, out ITextDocument textDocument) + { + if (textBuffer == null) + { + throw new ArgumentNullException("textBuffer"); + } + + textDocument = null; + + TextDocument document; + if (textBuffer.Properties.TryGetProperty(typeof(ITextDocument), out document)) + { + if(document != null && !document.IsDisposed) + { + textDocument = document; + return true; + } + else + { + Debug.Fail("There shouldn't be a null or disposed document in the buffer's property bag. Did someone else put it there?"); + } + } + + return false; + } + + public event EventHandler<TextDocumentEventArgs> TextDocumentCreated; + + public event EventHandler<TextDocumentEventArgs> TextDocumentDisposed; + + #endregion + + #region helpers + + /// <summary> + /// Helper method to raise the <see cref="ITextDocumentFactoryService.TextDocumentCreated"/> event. + /// </summary> + /// <param name="textDocument">The <see cref="ITextDocument"/> that was created.</param> + private void RaiseTextDocumentCreated(ITextDocument textDocument) + { + EventHandler<TextDocumentEventArgs> documentCreated = this.TextDocumentCreated; + if (documentCreated != null) + { + documentCreated.Invoke(this, new TextDocumentEventArgs(textDocument)); + } + } + + /// <summary> + /// Helper method to raise the <see cref="ITextDocumentFactoryService.TextDocumentDisposed"/> event. + /// </summary> + /// <param name="textDocument">The <see cref="ITextDocument"/> that was disposed.</param> + internal void RaiseTextDocumentDisposed(ITextDocument textDocument) + { + EventHandler<TextDocumentEventArgs> documentDisposed = this.TextDocumentDisposed; + if (documentDisposed != null) + { + documentDisposed.Invoke(this, new TextDocumentEventArgs(textDocument)); + } + } + + private IList<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> _orderedEncodingDetectors; + + internal IEnumerable<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> OrderedEncodingDetectors + { + get + { + if (_orderedEncodingDetectors == null) + { + if (UnorderedEncodingDetectors != null) + { + _orderedEncodingDetectors = Orderer.Order(UnorderedEncodingDetectors); + } + else + { + _orderedEncodingDetectors = new List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>>(); + } + } + return _orderedEncodingDetectors; + } + set // for unit test helper. + { + _orderedEncodingDetectors = new List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>>(value); + } + } + + // Exposed for testing. + internal Func<string, Stream> StreamCreator; + + private Stream OpenFile(string filePath, out DateTime lastModifiedTimeUtc, out long fileSize) + { + if (StreamCreator != null) + { + lastModifiedTimeUtc = DateTime.UtcNow; + fileSize = -1; // a signal that the file size is not known + return StreamCreator(filePath); + } + else + { + return OpenFileGuts(filePath, out lastModifiedTimeUtc, out fileSize); + } + } + + internal static Stream OpenFileGuts(string filePath, out DateTime lastModifiedTimeUtc, out long fileSize) + { + // Sometimes files are held open with FILE_FLAG_DELETE_ON_CLOSE before the editor + // is asked to open them. We should support that by allowing FileShare.Delete. + Stream result = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete); + FileInfo fileInfo = new FileInfo(filePath); + lastModifiedTimeUtc = fileInfo.LastWriteTimeUtc; + fileSize = fileInfo.Length; + if (fileSize > int.MaxValue) + { + throw new InvalidOperationException(Strings.FileTooLarge); + } + + return result; + } + + // For unit testing purposes + internal void Initialize(ITextBufferFactoryService bufferFactoryService) + { + Initialize(bufferFactoryService, null); + } + + internal void Initialize(ITextBufferFactoryService bufferFactoryService, List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>> detectors) + { + BufferFactoryService = bufferFactoryService; + UnorderedEncodingDetectors = detectors ?? new List<Lazy<IEncodingDetector, IEncodingDetectorMetadata>>(); + } + + #endregion + } +} diff --git a/src/Text/Impl/TextModel/TextImageVersion.cs b/src/Text/Impl/TextModel/TextImageVersion.cs new file mode 100644 index 0000000..ef3e44a --- /dev/null +++ b/src/Text/Impl/TextModel/TextImageVersion.cs @@ -0,0 +1,127 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +using System; +using System.Threading; + +namespace Microsoft.VisualStudio.Text.Implementation +{ + /// <summary> + /// An immutable variation on the StringBuilder class. + /// </summary> + internal class TextImageVersion : ITextImageVersion + { + public TextImageVersion(int length) + : this(versionNumber: 0, reiteratedVersionNumber: 0, length: length, identifier: new object()) + { + } + + private TextImageVersion(int versionNumber, int reiteratedVersionNumber, int length, object identifier) + { + this.VersionNumber = versionNumber; + this.ReiteratedVersionNumber = reiteratedVersionNumber; + this.Identifier = identifier; + this.Length = length; + } + + internal TextImageVersion CreateNext(int reiteratedVersionNumber, int length, INormalizedTextChangeCollection changes) + { + int newVersionNumber = this.VersionNumber + 1; + + if (reiteratedVersionNumber < 0) + { + // If there are no changes (e.g. readonly region edit or content type change), then + // we consider this a reiteration of the current version. changes can be null in the special case + // of doing a reload (at which point the reload code will call SetChanges after computing the diff). + reiteratedVersionNumber = ((changes != null) && (changes.Count == 0)) ? this.ReiteratedVersionNumber : newVersionNumber; + } + else if (reiteratedVersionNumber > newVersionNumber) + { + throw new ArgumentOutOfRangeException(nameof(reiteratedVersionNumber)); + } + + if (length == -1) + { + length = this.Length; + int changeCount = changes.Count; + for (int c = 0; c < changeCount; ++c) + { + length += changes[c].Delta; + } + } + + var newVersion = new TextImageVersion(newVersionNumber, reiteratedVersionNumber, length, this.Identifier); + + // Arguably this should happen as an atomic operation but it is unlikely to cause a race condition + // because, in general, people won't even be looking at these properties until they get a change event + // (which happens after everything has been set). + this.SetChanges(changes); + this.Next = newVersion; + + return newVersion; + } + + // The length needs to be set after the creation of the version in some cases (projection, for example). + internal void SetLength(int length) + { + if (this.Length != 0) + throw new InvalidOperationException("Not allowed to SetLength twice"); + + this.Length = length; + } + + internal void SetChanges(INormalizedTextChangeCollection changes) + { + if (this.Changes != null) + throw new InvalidOperationException("Not allowed to SetChanges twice"); + + this.Changes = changes; + } + + #region ITextImageVersion members + public ITextImageVersion Next { get; private set; } + + public int Length { get; private set; } + + public INormalizedTextChangeCollection Changes { get; private set; } + + public int VersionNumber { get; } + + public int ReiteratedVersionNumber { get; } + + public object Identifier { get; } + + public int TrackTo(VersionedPosition other, PointTrackingMode mode) + { + if (other.Version == null) + throw new ArgumentException(nameof(other)); + + if (other.Version.VersionNumber == this.VersionNumber) + return other.Position; + + if (other.Version.VersionNumber > this.VersionNumber) + return Tracking.TrackPositionForwardInTime(mode, other.Position, this, other.Version); + else + return Tracking.TrackPositionBackwardInTime(mode, other.Position, this, other.Version); + } + + public Span TrackTo(VersionedSpan span, SpanTrackingMode mode) + { + if (span.Version == null) + throw new ArgumentException(nameof(span)); + + if (span.Version.VersionNumber == this.VersionNumber) + return span.Span; + + if (span.Version.VersionNumber > this.VersionNumber) + return Tracking.TrackSpanForwardInTime(mode, span.Span, this, span.Version); + else + return Tracking.TrackSpanBackwardInTime(mode, span.Span, this, span.Version); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/TextModelUtilities.cs b/src/Text/Impl/TextModel/TextModelUtilities.cs new file mode 100644 index 0000000..8e42fb2 --- /dev/null +++ b/src/Text/Impl/TextModel/TextModelUtilities.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using Microsoft.VisualStudio.Text.Utilities; + + internal static class TextModelUtilities + { + static public int ComputeLineCountDelta(LineBreakBoundaryConditions boundaryConditions, StringRebuilder oldText, StringRebuilder newText) + { + int delta = 0; + delta -= oldText.LineBreakCount; + delta += newText.LineBreakCount; + if ((boundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) != 0) + { + if (oldText.FirstCharacter == '\n') + { + delta++; + } + if (newText.FirstCharacter == '\n') + { + delta--; + } + } + + if ((boundaryConditions & LineBreakBoundaryConditions.SucceedingNewline) != 0) + { + if (oldText.LastCharacter == '\r') + { + delta++; + } + if (newText.LastCharacter == '\r') + { + delta--; + } + } + + if ((oldText.Length == 0) && + ((boundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) != 0) && + ((boundaryConditions & LineBreakBoundaryConditions.SucceedingNewline) != 0)) + { + // return and newline were adjacent before and were separated by the insertion + delta++; + } + + if ((newText.Length == 0) && + ((boundaryConditions & LineBreakBoundaryConditions.PrecedingReturn) != 0) && + ((boundaryConditions & LineBreakBoundaryConditions.SucceedingNewline) != 0)) + { + // return and newline were separated before and were made adjacent by the deletion + delta--; + } + return delta; + } + } +} diff --git a/src/Text/Impl/TextModel/TextSnapshot.cs b/src/Text/Impl/TextModel/TextSnapshot.cs new file mode 100644 index 0000000..c331e01 --- /dev/null +++ b/src/Text/Impl/TextModel/TextSnapshot.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Collections.Generic; + + internal partial class TextSnapshot : BaseSnapshot, ITextSnapshot, ITextSnapshot2 + { + #region Private members + private readonly ITextBuffer textBuffer; + #endregion + + #region Constructors + public TextSnapshot(ITextBuffer textBuffer, ITextVersion2 version, StringRebuilder content) + : base(version, content) + { + System.Diagnostics.Debug.Assert(version.Length == content.Length); + this.textBuffer = textBuffer; + } + #endregion + + protected override ITextBuffer TextBufferHelper + { + get { return this.textBuffer; } + } + } +} diff --git a/src/Text/Impl/TextModel/TextSnapshotLine.cs b/src/Text/Impl/TextModel/TextSnapshotLine.cs new file mode 100644 index 0000000..c2a76e6 --- /dev/null +++ b/src/Text/Impl/TextModel/TextSnapshotLine.cs @@ -0,0 +1,164 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + using System.Diagnostics; + + internal partial class TextSnapshotLine : ITextSnapshotLine + { + private readonly int lineNumber; + private readonly int lineBreakLength; + private readonly SnapshotSpan extent; + + public TextSnapshotLine(ITextSnapshot snapshot, int lineNumber, Span extent, int lineBreakLength) + { + this.extent = new SnapshotSpan(snapshot, extent); + + //This is inner loop code called only from private methods so we don't need to guard against bad data in released bits. + Debug.Assert(extent.End + lineBreakLength <= snapshot.Length); + + this.lineNumber = lineNumber; + this.lineBreakLength = lineBreakLength; + } + + + public TextSnapshotLine(ITextSnapshot snapshot, TextImageLine lineSpan) + : this(snapshot, lineSpan.LineNumber, lineSpan.Extent, lineSpan.LineBreakLength) + { + } + + public TextSnapshotLine(ITextSnapshot snapshot, Tuple<int, Span, int> lineSpan) + : this(snapshot, lineSpan.Item1, lineSpan.Item2, lineSpan.Item3) + { + } + + /// <summary> + /// ITextSnapshot in which the line appears. + /// </summary> + public ITextSnapshot Snapshot + { + get { return this.extent.Snapshot; } + } + + /// <summary> + /// The 0-origin line number of the line. + /// </summary> + public int LineNumber + { + get + { + return this.lineNumber; + } + } + + /// <summary> + /// Position in TextBuffer of the first character in the line. + /// </summary> + public SnapshotPoint Start + { + get { return this.extent.Start; } + } + + /// <summary> + /// Length of the line, excluding any line break characters. + /// </summary> + public int Length + { + get { return this.extent.Length; } + } + + /// <summary> + /// Length of the line, including any line break characters. + /// </summary> + public int LengthIncludingLineBreak + { + get { return this.extent.Length + this.lineBreakLength; } + } + + /// <summary> + /// Length of line break characters (always falls in the range [0..2]) + /// </summary> + public int LineBreakLength + { + get { return this.lineBreakLength; } + } + + /// <summary> + /// The position of the first character past the end of the line, excluding any + /// line break characters (thus will address a line break character, except + /// for the last line in the buffer). + /// </summary> + public SnapshotPoint End + { + get { return this.extent.End; } + } + + /// <summary> + /// The position of the first character past the end of the line, including any + /// line break characters (thus will address the first character in + /// the succeeding line, unless this is the last line). + /// </summary> + public SnapshotPoint EndIncludingLineBreak + { + get { return new SnapshotPoint(this.extent.Snapshot, this.extent.Span.End + this.lineBreakLength); } + } + + /// <summary> + /// The extent of the line, excluding any line break characters. + /// </summary> + public SnapshotSpan Extent + { + get + { + return this.extent; + } + } + + /// <summary> + /// The extent of the line, including any line break characters. + /// </summary> + public SnapshotSpan ExtentIncludingLineBreak + { + get { return new SnapshotSpan(this.extent.Start, this.LengthIncludingLineBreak); } + } + + /// <summary> + /// The text of the line, excluding any line break characters. + /// May return incorrect results or fail if the text buffer has changed since the + /// ITextBufferLine was created. + /// </summary> + public string GetText() + { + return this.Extent.GetText(); + } + + /// <summary> + /// The text of the line, including any line break characters. + /// May return incorrect results of fail if the text buffer has changed since the + /// ITextBufferLine was created. + /// </summary> + /// <returns></returns> + public string GetTextIncludingLineBreak() + { + return this.ExtentIncludingLineBreak.GetText(); + } + + /// <summary> + /// The string consisting of the line break characters (if any) at the + /// end of the line. Has zero length for the last line in the buffer. + /// May return incorrect results of fail if the text buffer has changed since the + /// ITextBufferLine was created. + /// </summary> + /// <returns></returns> + public string GetLineBreakText() + { + return this.extent.Snapshot.GetText(new Span(this.Extent.Span.End, this.lineBreakLength)); + } + } +} diff --git a/src/Text/Impl/TextModel/TextVersion.cs b/src/Text/Impl/TextModel/TextVersion.cs new file mode 100644 index 0000000..55727c5 --- /dev/null +++ b/src/Text/Impl/TextModel/TextVersion.cs @@ -0,0 +1,189 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + + /// <summary> + /// An internal implementation of ITextVersion + /// </summary> + internal partial class TextVersion : ITextVersion, ITextVersion2 + { + private readonly TextImageVersion _textImageVersion; + + /// <summary> + /// Initializes a new instance of a <see cref="TextVersion"/>. + /// </summary> + /// <param name="textBuffer">The <see cref="ITextBuffer"/> to which the version belongs.</param> + /// <param name="imageVersion">The <see cref="ITextImageVersion"/> of the associated snapshot.</param> + public TextVersion(ITextBuffer textBuffer, TextImageVersion imageVersion) + { + if (textBuffer == null) + { + throw new ArgumentNullException(nameof(textBuffer)); + } + + if (imageVersion == null) + { + throw new ArgumentNullException(nameof(imageVersion)); + } + + this.TextBuffer = textBuffer; + _textImageVersion = imageVersion; + } + + /// <summary> + /// Create a new version based on applying <paramref name="changes"/> to this. + /// </summary> + /// <param name="changes">null if set later</param> + /// <param name="newLength">use -1 to compute a length</param> + /// <param name="reiteratedVersionNumber">use -1 to get the default value</param> + /// <remarks> + /// <para>If <paramref name="changes"/> can be null, then <paramref name="newLength"/> cannot be -1.</para> + /// </remarks> + internal TextVersion CreateNext(INormalizedTextChangeCollection changes, int newLength = -1, int reiteratedVersionNumber = -1) + { + if (this.Next != null) + throw new InvalidOperationException("Not allowed to CreateNext twice"); + + var newTextImageVersion = this._textImageVersion.CreateNext(reiteratedVersionNumber: reiteratedVersionNumber, length: newLength, changes: changes); + + var next = new TextVersion(this.TextBuffer, newTextImageVersion); + this.Next = next; + + return next; + } + + internal void SetLength(int length) + { + _textImageVersion.SetLength(length); + } + + internal void SetChanges(INormalizedTextChangeCollection changes) + { + _textImageVersion.SetChanges(changes); + } + + public ITextBuffer TextBuffer + { + get; + } + + public int VersionNumber + { + get { return this.ImageVersion.VersionNumber; } + } + + public int ReiteratedVersionNumber + { + get { return this.ImageVersion.ReiteratedVersionNumber; } + } + + /// <summary> + /// Gets the next version node + /// </summary> + public ITextVersion Next + { + get; private set; + } + + /// <summary> + /// Gets the current change information + /// </summary> + public INormalizedTextChangeCollection Changes + { + get { return this.ImageVersion.Changes; } + } + + public int Length + { + get { return this.ImageVersion.Length; } + } + + public ITextImageVersion ImageVersion { get { return _textImageVersion; } } + + #region Point and Span Factories + public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode) + { + // Forward fidelity is implicit + return new ForwardFidelityTrackingPoint(this, position, trackingMode); + } + + public ITrackingPoint CreateTrackingPoint(int position, PointTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + if (trackingFidelity == TrackingFidelityMode.Forward) + { + return new ForwardFidelityTrackingPoint(this, position, trackingMode); + } + else + { + return new HighFidelityTrackingPoint(this, position, trackingMode, trackingFidelity); + } + } + + public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode) + { + // Forward fidelity is implicit + if (trackingMode == SpanTrackingMode.Custom) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + return new ForwardFidelityTrackingSpan(this, new Span(start, length), trackingMode); + } + + public ITrackingSpan CreateTrackingSpan(int start, int length, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + return CreateTrackingSpan(new Span(start, length), trackingMode, trackingFidelity); + } + + public ITrackingSpan CreateTrackingSpan(Span span, SpanTrackingMode trackingMode) + { + // Forward fidelity is implicit + if (trackingMode == SpanTrackingMode.Custom) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + return new ForwardFidelityTrackingSpan(this, span, trackingMode); + } + + public ITrackingSpan CreateTrackingSpan(Span span, SpanTrackingMode trackingMode, TrackingFidelityMode trackingFidelity) + { + if (trackingMode == SpanTrackingMode.Custom) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + if (trackingFidelity == TrackingFidelityMode.Forward) + { + return new ForwardFidelityTrackingSpan(this, span, trackingMode); + } + else + { + return new HighFidelityTrackingSpan(this, span, trackingMode, trackingFidelity); + } + } + + public ITrackingSpan CreateCustomTrackingSpan(Span span, TrackingFidelityMode trackingFidelity, object customState, CustomTrackToVersion behavior) + { + if (behavior == null) + { + throw new ArgumentNullException("behavior"); + } + if (trackingFidelity != TrackingFidelityMode.Forward) + { + throw new NotImplementedException(); + } + return new ForwardFidelityCustomTrackingSpan(this, span, customState, behavior); + } + #endregion + + public override string ToString() + { + return String.Format("V{0} (r{1})", VersionNumber, ReiteratedVersionNumber); + } + } +} diff --git a/src/Text/Impl/TextModel/TrackingPoint.cs b/src/Text/Impl/TextModel/TrackingPoint.cs new file mode 100644 index 0000000..02750f3 --- /dev/null +++ b/src/Text/Impl/TextModel/TrackingPoint.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. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + + /// <summary> + /// Base class for a tracking position in a particular <see cref="ITextBuffer"/>. + /// </summary> + internal abstract partial class TrackingPoint : ITrackingPoint + { + #region State and Construction + protected readonly PointTrackingMode trackingMode; + + protected TrackingPoint(ITextVersion version, int position, PointTrackingMode trackingMode) + { + if (version == null) + { + throw new ArgumentNullException("version"); + } + if (position < 0 | position > version.Length) + { + throw new ArgumentOutOfRangeException("position"); + } + if (trackingMode < PointTrackingMode.Positive || trackingMode > PointTrackingMode.Negative) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + + this.trackingMode = trackingMode; + } + #endregion + + #region ITrackingPoint members + public abstract ITextBuffer TextBuffer { get; } + + public PointTrackingMode TrackingMode + { + get { return this.trackingMode; } + } + + public abstract TrackingFidelityMode TrackingFidelity { get; } + + public int GetPosition(ITextVersion version) + { + if (version == null) + { + throw new ArgumentNullException("version"); + } + if (version.TextBuffer != this.TextBuffer) + { + throw new ArgumentException(Strings.InvalidVersion); + } + return TrackPosition(version); + } + + public int GetPosition(ITextSnapshot snapshot) + { + if (snapshot == null) + { + throw new ArgumentNullException("snapshot"); + } + if (snapshot.TextBuffer != this.TextBuffer) + { + throw new ArgumentException(Strings.InvalidSnapshot); + } + return TrackPosition(snapshot.Version); + } + + public SnapshotPoint GetPoint(ITextSnapshot snapshot) + { + return new SnapshotPoint(snapshot, GetPosition(snapshot)); + } + + public char GetCharacter(ITextSnapshot snapshot) + { + return GetPoint(snapshot).GetChar(); + } + #endregion + + protected abstract int TrackPosition(ITextVersion targetVersion); + + #region Diagnostic Support + protected static string PointTrackingModeToString(PointTrackingMode trackingMode) + { + return trackingMode == PointTrackingMode.Positive ? "→" : "←"; + } + + protected static string ToString(ITextVersion version, int position, PointTrackingMode trackingMode) + { + return string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0} {2}@{1}", + version.VersionNumber, position, PointTrackingModeToString(trackingMode)); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/TrackingSpan.cs b/src/Text/Impl/TextModel/TrackingSpan.cs new file mode 100644 index 0000000..8ffb5fe --- /dev/null +++ b/src/Text/Impl/TextModel/TrackingSpan.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + + /// <summary> + /// Base class for a tracking span in a particular <see cref="ITextBuffer"/>. + /// </summary> + internal abstract partial class TrackingSpan : ITrackingSpan + { + #region State and Construction + protected readonly SpanTrackingMode trackingMode; + + public TrackingSpan(ITextVersion version, Span span, SpanTrackingMode trackingMode) + { + if (version == null) + { + throw new ArgumentNullException("version"); + } + if (span.End > version.Length) + { + throw new ArgumentOutOfRangeException("span"); + } + if (trackingMode < SpanTrackingMode.EdgeExclusive || trackingMode > SpanTrackingMode.Custom) + { + throw new ArgumentOutOfRangeException("trackingMode"); + } + + this.trackingMode = trackingMode; + } + #endregion + + #region ITrackingSpan members + public abstract ITextBuffer TextBuffer { get; } + + public SpanTrackingMode TrackingMode + { + get { return this.trackingMode; } + } + + public abstract TrackingFidelityMode TrackingFidelity { get; } + + public Span GetSpan(ITextVersion version) + { + if (version == null) + { + throw new ArgumentNullException("version"); + } + if (version.TextBuffer != this.TextBuffer) + { + throw new ArgumentException(Strings.InvalidVersion); + } + return TrackSpan(version); + } + + public SnapshotSpan GetSpan(ITextSnapshot snapshot) + { + if (snapshot == null) + { + throw new ArgumentNullException("snapshot"); + } + if (snapshot.TextBuffer != this.TextBuffer) + { + throw new ArgumentException(Strings.InvalidSnapshot); + } + + return new SnapshotSpan(snapshot, TrackSpan(snapshot.Version)); + } + + public SnapshotPoint GetStartPoint(ITextSnapshot snapshot) + { + SnapshotSpan s = this.GetSpan(snapshot); + return new SnapshotPoint(snapshot, s.Start); + } + + public SnapshotPoint GetEndPoint(ITextSnapshot snapshot) + { + SnapshotSpan s = this.GetSpan(snapshot); + return new SnapshotPoint(snapshot, s.End); + } + + public string GetText(ITextSnapshot snapshot) + { + return GetSpan(snapshot).GetText(); + } + #endregion + + #region Helpers + protected abstract Span TrackSpan(ITextVersion targetVersion); + #endregion + + #region Diagnostic Support + protected static string SpanTrackingModeToString(SpanTrackingMode trackingMode) + { + switch (trackingMode) + { + case SpanTrackingMode.EdgeExclusive: + return "→←"; + case SpanTrackingMode.EdgeInclusive: + return "←→"; + case SpanTrackingMode.EdgeNegative: + return "←←"; + case SpanTrackingMode.EdgePositive: + return "→→"; + case SpanTrackingMode.Custom: + return "custom"; + default: + return "??"; + } + } + + protected static string ToString(ITextVersion version, Span span, SpanTrackingMode trackingMode) + { + return string.Format(System.Globalization.CultureInfo.CurrentCulture, "V{0} {2}@{1}", + version.VersionNumber, span.ToString(), SpanTrackingModeToString(trackingMode)); + } + #endregion + } +} diff --git a/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs b/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs new file mode 100644 index 0000000..bb70ebd --- /dev/null +++ b/src/Text/Impl/TextModel/TrivialNormalizedTextChangeCollection.cs @@ -0,0 +1,236 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + using System; + + /// <summary> + /// A compact representation of a normalized text change collection containing a single change that is the + /// insertion, deletion, or replacement of one character, with no effect on line counts. This object embodies both the + /// collection and its single member. + /// </summary> + internal partial class TrivialNormalizedTextChangeCollection : INormalizedTextChangeCollection, ITextChange3 + { + char data; + bool isInsertion; + int position; + + public TrivialNormalizedTextChangeCollection(char data, bool isInsertion, int position) + { + this.data = data; + this.isInsertion = isInsertion; + this.position = position; + } + + public bool IncludesLineChanges + { + get { return false; } + } + + #region IList<ITextChange> implementation + public ITextChange this[int index] + { + get + { + if (index != 0) + { + throw new ArgumentOutOfRangeException("index"); + } + return this; + } + set + { + throw new System.NotSupportedException(); + } + } + + public void Insert(int index, ITextChange item) + { + throw new System.NotSupportedException(); + } + + public void RemoveAt(int index) + { + throw new System.NotSupportedException(); + } + #endregion + + #region ICollection<ITextChange> implementation + public int Count + { + get { return 1; } + } + + public bool IsReadOnly + { + get { return true; } + } + + public void Add(ITextChange item) + { + throw new System.NotSupportedException(); + } + + public void Clear() + { + throw new System.NotSupportedException(); + } + + public int IndexOf(ITextChange item) + { + return item == this ? 0 : -1; + } + + public bool Contains(ITextChange item) + { + return item == this; + } + + public void CopyTo(ITextChange[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException("array"); + } + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException("arrayIndex"); + } + if (array.Rank > 1 || arrayIndex >= array.Length) + { + throw new ArgumentException("Bad arguments to CopyTo"); + } + array[arrayIndex] = this; + } + + public bool Remove(ITextChange item) + { + throw new System.NotSupportedException(); + } + #endregion + + #region IEnumerable<ITextChange> implementation + public System.Collections.Generic.IEnumerator<ITextChange> GetEnumerator() + { + yield return this; + } + #endregion + + #region IEnumerable implementation + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + yield return this; + } + #endregion + + #region ITextChange implementation + Span ITextChange.OldSpan + { + get { return new Span(position, isInsertion ? 0 : 1); } + } + + Span ITextChange.NewSpan + { + get { return new Span(position, isInsertion ? 1 : 0); } + } + + int ITextChange.OldPosition + { + get { return position; } + } + + int ITextChange.NewPosition + { + get { return position; } + } + + int ITextChange.Delta + { + get { return isInsertion ? +1 : -1; } + } + + int ITextChange.OldEnd + { + get { return isInsertion ? position : position + 1; } + } + + int ITextChange.NewEnd + { + get { return isInsertion ? position + 1 : position; } + } + + string ITextChange.OldText + { + get { return isInsertion ? "" : new string(data, 1); } + } + + string ITextChange.NewText + { + get { return isInsertion ? new string(data, 1) : ""; } + } + + int ITextChange.OldLength + { + get { return isInsertion ? 0 : 1; } + } + + int ITextChange.NewLength + { + get { return isInsertion ? 1 : 0; } + } + + int ITextChange.LineCountDelta + { + get { return 0; } + } + + public bool IsOpaque { get; internal set; } + + public string GetOldText(Span span) + { + if (span.End > 1) + { + throw new ArgumentOutOfRangeException(nameof(span)); + } + + return isInsertion ? "" : new string(data, span.Length); + } + + public string GetNewText(Span span) + { + if (span.End > 1) + { + throw new ArgumentOutOfRangeException(nameof(span)); + } + + return isInsertion ? new string(data, span.Length) : ""; + } + + public char GetOldTextAt(int position) + { + if (position > 0 || isInsertion) + { + throw new ArgumentOutOfRangeException(nameof(position)); + } + + return data; + } + + public char GetNewTextAt(int position) + { + if (position > 0 || !isInsertion) + { + throw new ArgumentOutOfRangeException(nameof(position)); + } + + return data; + } + + #endregion + } +}
\ No newline at end of file diff --git a/src/Text/Impl/TextModel/VersionNumberPosition.cs b/src/Text/Impl/TextModel/VersionNumberPosition.cs new file mode 100644 index 0000000..47ee7b2 --- /dev/null +++ b/src/Text/Impl/TextModel/VersionNumberPosition.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Implementation +{ + /// <summary> + /// Describe a version number and a position in that version. Used in the implementation + /// of high fidelity tracking points and spans. + /// </summary> + internal struct VersionNumberPosition + { + public int VersionNumber; + public int Position; + public VersionNumberPosition(int versionNumber, int position) + { + this.VersionNumber = versionNumber; + this.Position = position; + } + } + + internal class VersionNumberPositionComparer : System.Collections.Generic.IComparer<VersionNumberPosition> + { + public int Compare(VersionNumberPosition x, VersionNumberPosition y) + { + return x.VersionNumber - y.VersionNumber; // both values are nonnegative, no overflow possible + } + + static public VersionNumberPositionComparer Instance = new VersionNumberPositionComparer(); + } +} diff --git a/src/Text/Impl/TextSearch/BackgroundSearch.cs b/src/Text/Impl/TextSearch/BackgroundSearch.cs new file mode 100644 index 0000000..f0e6db9 --- /dev/null +++ b/src/Text/Impl/TextSearch/BackgroundSearch.cs @@ -0,0 +1,450 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Find.Implementation +{ + using Microsoft.VisualStudio.Text.Operations; + using Microsoft.VisualStudio.Text.Tagging; + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + + /// <summary> + /// Performs text searches on lowest priority background threads and caches the results. + /// </summary> + /// <remarks> + /// The goal here is to be completely thread-safe: searches can be requested from any thread (and actually happen on background threads). + /// + /// Another goal is to never search more than we need to: + /// Once we've searched a section of the buffer we don't search it again unless it is modified. + /// Even if we get multiple, nearly simultaneous requests to search a section of the buffer, we only search it once. + /// </remarks> + internal class BackgroundSearch<T> : IDisposable where T : ITag + { + private ITextBuffer _buffer; + private readonly ITextSearchService2 _textSearchService; + private readonly string _searchTerm; + private readonly FindOptions _options; + public readonly Func<SnapshotSpan, T> TagFactory; + private readonly Action<ITextSnapshot, NormalizedSpanCollection> _callback; + private bool _isDisposed; + + //This is used for locks so it can never be deleted/recreated. Internal for unit tests. + internal readonly Queue<NormalizedSnapshotSpanCollection> _requestQueue = new Queue<NormalizedSnapshotSpanCollection>(); + + //This needs to update atomically and is internal so unit tests can (more) easily test edge cases. + internal SearchResults _results; + + public BackgroundSearch(ITextSearchService2 textSearchService, ITextBuffer buffer, string searchTerm, FindOptions options, + Func<SnapshotSpan, T> tagFactory, Action<ITextSnapshot, NormalizedSpanCollection> callback) + { + _textSearchService = textSearchService; + _buffer = buffer; + + _searchTerm = searchTerm; + _options = options & ~FindOptions.SearchReverse; //The tagger ignores the reversed flag. + this.TagFactory = tagFactory; + _callback = callback; + + _results = new SearchResults(_buffer.CurrentSnapshot, NormalizedSpanCollection.Empty, NormalizedSpanCollection.Empty); + } + + public NormalizedSnapshotSpanCollection Results + { + get + { + var results = _results; //Snapshot results to avoid taking a lock. + return new NormalizedSnapshotSpanCollection(results.Snapshot, results.Matches); + } + } + + /// <summary> + /// Kick off a background search if we don't have current results. Do nothing otherwise. + /// </summary> + /// <remarks> + /// This method can be called from any thread (though it will generally only be called from the UI thread). + /// </remarks> + public void QueueSearch(NormalizedSnapshotSpanCollection requestedSnapshotSpans) + { + Debug.Assert(requestedSnapshotSpans.Count > 0); + + //Check to see if we have completely searched the current version of the text buffer + //and quickly abort since there is no point in queuing up another search if we have. + var results = _results; //Snapshot results to avoid taking a lock. + if (results.Snapshot == _buffer.CurrentSnapshot) + { + if ((results.SearchedSpans.Count == 1) && (results.SearchedSpans[0].Start == 0) && (results.SearchedSpans[0].Length == results.Snapshot.Length)) + { + //We've searched the entire snapshot. + return; + } + + if (requestedSnapshotSpans[0].Snapshot == results.Snapshot) + { + NormalizedSpanCollection unsearchedRequest = NormalizedSpanCollection.Difference(requestedSnapshotSpans, results.SearchedSpans); + if (unsearchedRequest.Count == 0) + { + return; + } + } + } + + lock (_requestQueue) + { + _requestQueue.Enqueue(requestedSnapshotSpans); + if (_requestQueue.Count != 1) + { + //Request has been queued & we already have an active thread processing requests. + return; + } + } + + Task.Factory.StartNew(this.ProcessQueue, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default); + } + + #region Private Helpers + internal void ProcessQueue() + { + // Ensure the thread that is doing the work is both low priority and also background + try + { + Thread.CurrentThread.Priority = ThreadPriority.Lowest; + + //Only one instance of this thread is running at a time, so we don't need to put locks around the bits that update + //our state (only the bits that play with the results queue). + while (true) + { + if (_isDisposed) + return; + + NormalizedSnapshotSpanCollection request; + lock (_requestQueue) + { + //Do not dequeue the result here ... if a new request comes in while we are processing this request, + //we do not want to start a new thread. + request = _requestQueue.Peek(); + } + + //Always do searches on the current snapshot of the buffer, migrating results to that snapshot + //if needed. + ITextSnapshot snapshot = this.AdvanceToCurrentSnapshot(); + + NormalizedSpanCollection requestedSpans; + if (_options.HasFlag(FindOptions.Multiline)) + { + //Multi-line searches are all or nothing. + if (_results.SearchedSpans.Count == 0) + { + requestedSpans = new NormalizedSpanCollection(new Span(0, snapshot.Length)); + } + else + { + Debug.Assert((_results.SearchedSpans.Count == 1) && (_results.SearchedSpans[0].Start == 0) && (_results.SearchedSpans[0].End == snapshot.Length)); + requestedSpans = NormalizedSpanCollection.Empty; + } + } + else + { + requestedSpans = BackgroundSearch<T>.TranslateToAndExtend(request[0].Snapshot, request, snapshot); + + if (_results.SearchedSpans.Count > 0) + { + if ((_results.SearchedSpans.Count == 1) && (_results.SearchedSpans[0].Start == 0) && (_results.SearchedSpans[0].End == snapshot.Length)) + { + //We've already got results for the entire buffer. + requestedSpans = NormalizedSpanCollection.Empty; + } + else + { + requestedSpans = NormalizedSpanCollection.Difference(requestedSpans, _results.SearchedSpans); + } + } + } + + bool dequeueRequest = true; + if (requestedSpans.Count > 0) + { + IList<Span> newMatches = this.FindAll(snapshot, requestedSpans); + + if (_isDisposed) + return; + + if (snapshot == _buffer.CurrentSnapshot) + { + //The search completed without the buffer changing out from under us, add in the new results. + //Remove any stale results in the places we searched (since we do not remove potentially stale results + //on a text change, we have to remove them here) and then add in the results we found. + if (_options.HasFlag(FindOptions.Multiline)) + { + //Multiline searches are always whole buffer searches, so we can skip the set operations. + Debug.Assert(requestedSpans.Count == 1); + Debug.Assert(requestedSpans[0].Start == 0); + Debug.Assert(requestedSpans[0].Length == snapshot.Length); + + _results = new SearchResults(snapshot, + new NormalizedSpanCollection(newMatches), + new NormalizedSpanCollection(new Span(0, snapshot.Length))); + } + else + { + //Remove the stale results. + NormalizedSpanCollection m = NormalizedSpanCollection.Difference(_results.Matches, requestedSpans); + + //Add in the new results. + if (newMatches.Count > 0) + { + m = NormalizedSpanCollection.Union(m, new NormalizedSpanCollection(newMatches)); + } + + //Save the results + _results = new SearchResults(snapshot, + m, + NormalizedSpanCollection.Union(_results.SearchedSpans, requestedSpans)); + } + + //We completed the search & updated the results ... have the tagger to raise the appropriate changed event + //on the span we just searched. + // + //We can't raise the tags changed on just the results since we also need to signal that stale results have + //been removed. + _callback(snapshot, requestedSpans); + } + else + { + //The buffer changed so we can't trust the results we just got (the search may not have completed). + //Don't dequeue the request and we'll repeat the process (but on the correct snapshot). + dequeueRequest = false; + } + } + + if (dequeueRequest) + { + lock (_requestQueue) + { + //Nothing should have moved the request out of the queue. + Debug.Assert(object.ReferenceEquals(request, _requestQueue.Peek())); + + _requestQueue.Dequeue(); + + if (_requestQueue.Count == 0) + { + //No more requests are pending, release the worker thread. + return; + } + } + } + } + } + finally + { + Thread.CurrentThread.Priority = ThreadPriority.Normal; + } + } + + internal ITextSnapshot AdvanceToCurrentSnapshot() + { + //We don't need to take a snapshot of the results because the results are only modified on this thread. + ITextSnapshot oldSnapshot = _results.Snapshot; + ITextSnapshot newSnapshot = _buffer.CurrentSnapshot; + + if (oldSnapshot != newSnapshot) + { + //The results are all on an old snapshot. We need to project them forward (even though that might cause some stale and incorrect + //results). + NormalizedSpanCollection newMatches = TextSearchNavigator.TranslateTo(oldSnapshot, _results.Matches, newSnapshot); + NormalizedSpanCollection newSearchedSpans = NormalizedSpanCollection.Empty; + + if ((_results.SearchedSpans.Count != 0) && !_options.HasFlag(FindOptions.Multiline)) + { + //Advance our record of the spans that have already been searched to the new snapshot as well. + newSearchedSpans = BackgroundSearch<T>.TranslateToAndExtend(oldSnapshot, _results.SearchedSpans, newSnapshot); + + //But remove anything on a TextSnapshotLine that was modified by the change. + List<Span> changedSpansOnNewSnapshot = new List<Span>(); + ITextVersion version = oldSnapshot.Version; + while (version != newSnapshot.Version) + { + foreach (var change in version.Changes) + { + changedSpansOnNewSnapshot.Add(BackgroundSearch<T>.Extend(newSnapshot, Tracking.TrackSpanForwardInTime(SpanTrackingMode.EdgeInclusive, change.NewSpan, + version.Next, newSnapshot.Version))); + } + + version = version.Next; + } + + if (changedSpansOnNewSnapshot.Count > 0) + { + NormalizedSpanCollection changes = new NormalizedSpanCollection(changedSpansOnNewSnapshot); + + //Remove the spans touched by changes from the spans we've searched + newSearchedSpans = NormalizedSpanCollection.Difference(newSearchedSpans, changes); + } + } + + _results = new SearchResults(newSnapshot, newMatches, newSearchedSpans); + } + + return newSnapshot; + } + + public static NormalizedSpanCollection TranslateToAndExtend(ITextSnapshot currentSnapshot, NormalizedSpanCollection currentSpans, ITextSnapshot targetSnapshot) + { + if (currentSpans.Count == 0) + { + return currentSpans; + } + + List<Span> spans = new List<Span>(currentSpans.Count); + foreach (var s in currentSpans) + { + spans.Add(BackgroundSearch<T>.Extend(targetSnapshot, Tracking.TrackSpanForwardInTime(SpanTrackingMode.EdgeNegative, + s, + currentSnapshot.Version, targetSnapshot.Version))); + } + + return new NormalizedSpanCollection(spans); + } + + //Grow a snapshot span so that it includes all of the TextSnapshotLines that overlap the span (but always return at least one + //complete line). + public static Span Extend(ITextSnapshot snapshot, Span span) + { + ITextSnapshotLine start = snapshot.GetLineFromPosition(span.Start); + if (span.End <= start.EndIncludingLineBreak.Position) + { + //source.End is on the same line (or possibly the start of the next line) ... return just this line. + return start.ExtentIncludingLineBreak; + } + else + { + ITextSnapshotLine end = snapshot.GetLineFromPosition(span.End); + + //if source.End is at the start of the line, only return up to the start of the line, otherwise + //include the entire line). + return Span.FromBounds(start.Start, + (end.Start.Position == span.End) + ? end.Start + : end.EndIncludingLineBreak); + } + } + + /// <summary> + /// Simulates a search on the range where the user is performing a series of find next operations, buts aborts quickly + /// when either the BackgroundSearch advances to a new snapshot or is disposed. + /// </summary> + private IList<Span> FindAll(ITextSnapshot snapshot, NormalizedSpanCollection spans) + { + IList<Span> matches = new List<Span>(); + + int start = int.MinValue; + int end = int.MinValue; + + foreach (var span in spans) + { + //All the spans are normalized to conver entire text snapshot lines. + Debug.Assert(snapshot.GetLineFromPosition(span.Start).Start == span.Start); + Debug.Assert((span.End == snapshot.Length) || (snapshot.GetLineFromPosition(span.End).Start == span.End)); + + if (span.Length > 0) + { + SnapshotSpan searchRange = new SnapshotSpan(snapshot, span); + SnapshotPoint startingPosition = searchRange.Start; + + while (true) + { + if (_isDisposed || (snapshot != _buffer.CurrentSnapshot)) + { + //We've been disposed of or the buffer has advanced to a new snapshot. Either way, abort the search. + return matches; + } + + SnapshotSpan? match = _textSearchService.Find(searchRange, startingPosition, _searchTerm, _options); + + if (match.HasValue) + { + if (match.Value.Start > end) + { + //The current match is disjoint from the last match, add it to the list of matches. + if (end != int.MinValue) + { + matches.Add(Span.FromBounds(start, end)); + + //Avoid problems when there are so many matches (e.g. searching for 'a' in a 300MB file) that + //we run out of memory tracking results. + // + //The effect of this cut-out isn't exactly predictable (doing several smaller searches will + //allow the total number of results maintained by the background search class to grow past + //the limit but a large search will hit the limit and miss results). + if (matches.Count > 5000) + return matches; + } + + start = match.Value.Start; + end = match.Value.End; + } + else + { + //The new match overlaps the old. Simple extend the existing matched span. + end = Math.Max(end, match.Value.End); //With an RE, the new match could end before the end of the previous match. + } + + startingPosition = match.Value.Start; + + if (startingPosition >= span.End) + { + break; + } + else + { + startingPosition += 1; + } + } + else + { + break; + } + } + } + } + + if (end != int.MinValue) + { + matches.Add(Span.FromBounds(start, end)); + } + + return matches; + } + + #endregion + + #region IDisposable Members + + public void Dispose() + { + _isDisposed = true; + } + + #endregion + + internal class SearchResults + { + public readonly ITextSnapshot Snapshot; + public readonly NormalizedSpanCollection Matches; + public readonly NormalizedSpanCollection SearchedSpans; + + public SearchResults(ITextSnapshot snapshot, NormalizedSpanCollection matches, NormalizedSpanCollection searchedSpans) + { + this.Snapshot = snapshot; + this.Matches = matches; + this.SearchedSpans = searchedSpans; + } + } + } +} diff --git a/src/Text/Impl/TextSearch/TextSearchNavigator.cs b/src/Text/Impl/TextSearch/TextSearchNavigator.cs new file mode 100644 index 0000000..360e9e4 --- /dev/null +++ b/src/Text/Impl/TextSearch/TextSearchNavigator.cs @@ -0,0 +1,521 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Find.Implementation +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Operations; + + class TextSearchNavigator : ITextSearchNavigator2 + { + readonly ITextBuffer _buffer; + readonly ITextSearchService2 _textSearchService; + + public TextSearchNavigator(ITextSearchService2 textSearchService, ITextBuffer buffer) + { + _buffer = buffer; + _textSearchService = textSearchService; + } + + #region Private Helpers + + /// <summary> + /// Calculates the search start point for the next search operation. Always returns a point on the buffer's + /// current snapshot. If no point is returned, then we're at ends of the buffer (or search range) + /// and wrap is turned off. + /// </summary> + private SnapshotPoint? CalculateStartPoint(ITextSnapshot searchSnapshot, bool wrap, bool forward) + { + // If there is a current result, then use its span to figure out the next starting point, otherwise + // use the StartPoint itself + SnapshotSpan? currentResult = this.CurrentResult; + SnapshotPoint nextSearchStart; + + if (currentResult.HasValue) + { + int position; + if (forward) + { + // moving forwards (by default one more than the start of the previous match). + position = currentResult.Value.Start.Position + 1; + + if (position > searchSnapshot.Length) + { + // The previous search result was at the end of the buffer + if (wrap) + { + position = 0; + } + else + { + return null; + } + } + } + else + { + // Moving backwards (by default 1 less than the end of the previous match). + position = currentResult.Value.End.Position - 1; + if (position < 0) + { + // The last position was at the start of the buffer + if (wrap) + { + position = searchSnapshot.Length; + } + else + { + return null; + } + } + } + + nextSearchStart = new SnapshotPoint(currentResult.Value.Snapshot, position); + } + else if (this.StartPoint != null) + { + // We have no current result, simply use the starting point as the search point. + // If there is none, then use the start of the buffer as the search starting point. + nextSearchStart = this.StartPoint.Value; + } + else + { + //If all else fails, start at the start of the buffer. + nextSearchStart = new SnapshotPoint(_buffer.CurrentSnapshot, 0); + } + + return nextSearchStart.TranslateTo(searchSnapshot, GetTrackingMode(forward)); + } + #endregion + + #region ITextSearchNavigator Members + public string SearchTerm { get; set; } + + public string ReplaceTerm { get; set; } + + public FindOptions SearchOptions { get; set; } + + public SnapshotSpan? CurrentResult { get; private set; } + + private ITrackingSpan _searchSpan; + public ITrackingSpan SearchSpan + { + get + { + var span = _searchSpan; + if ((span == null) && (_searchSpans != null) && (_searchSpans.Count > 0)) + { + var snapshot = _searchSpans[0].Snapshot; + span = snapshot.CreateTrackingSpan(Span.FromBounds(_searchSpans[0].Start, _searchSpans[_searchSpans.Count - 1].End), SpanTrackingMode.EdgeInclusive); + } + return span; + } + set + { + if (value != null && value.TextBuffer != _buffer) + { + throw new InvalidOperationException("The SearchSpan must be on the same buffer as the navigator itself."); + } + + //We keep _searchSpan & _searchSpans (instead of converting this to a NormalizedSnapshotSpanCollection) because the user + //could have provided a custom tracking span and we'd loose the tracking behavior in the conversion. + _searchSpan = value; + _searchSpans = null; + } + } + + private SnapshotPoint? _startPoint; + public SnapshotPoint? StartPoint + { + get + { + UpdateStartPoint(); + return _startPoint; + } + set + { + if (value != null && value.Value.Snapshot.TextBuffer != _buffer) + { + throw new ArgumentException("StartPoint must be on the same buffer as the search navigator itself."); + } + + _startPoint = value; + } + } + + public bool Find() + { + if (string.IsNullOrEmpty(this.SearchTerm)) + { + throw new InvalidOperationException("You must set a non-empty search term before searching."); + } + + bool forward = (this.SearchOptions & FindOptions.SearchReverse) != FindOptions.SearchReverse; + bool wrap = (this.SearchOptions & FindOptions.Wrap) == FindOptions.Wrap; + bool regEx = (this.SearchOptions & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions; + + ITextSnapshot searchSnapshot = _buffer.CurrentSnapshot; + + //There could be a version skew here if someone calls find from inside a text changed callback on the buffer. That probably wouldn't be a good + //idea but we need to handle it gracefully. + this.AdvanceToSnapshot(searchSnapshot); + + SnapshotPoint? searchStart = this.CalculateStartPoint(searchSnapshot, wrap, forward); + if (searchStart.HasValue) + { + int index = 0; + NormalizedSnapshotSpanCollection searchSpans = this.SearchSpans; + + if (searchSpans != null) + { + Debug.Assert(searchSpans.Count > 0); + + //Index is potentially outside the range of [0...searchSpans.Count-1] but we handle that below. + if (!(TextSearchNavigator.TryGetIndexOfContainingSpan(searchSpans, searchStart.Value, out index) || forward)) + { + //For reversed searches, we want the index of the span before the point if we can't get a span that contains the point. + --index; + } + } + else + { + searchSpans = new NormalizedSnapshotSpanCollection(new SnapshotSpan(searchSnapshot, Span.FromBounds(0, searchSnapshot.Length))); + } + + int searchIterations = searchSpans.Count; + for (int i = 0; (i < searchIterations); ++i) + { + //index needs to be normalized to [0 ... searchSpans.Count - 1] but could be negative. + index = (index + searchSpans.Count) % searchSpans.Count; + + SnapshotSpan searchSpan = searchSpans[index]; + if ((i != 0) || (searchStart.Value < searchSpan.Start) || (searchStart.Value > searchSpan.End)) + { + searchStart = forward ? searchSpan.Start : searchSpan.End; + } + else if (wrap && (i == 0)) + { + //We will need to repeat the search to account for wrap being on and we are not searching everything in searchSpans[0]. + //This is the same as simply doing a search for i == searchSpans.Count we we can make happen by bumping the number of iterations. + ++searchIterations; + } + + foreach (var result in _textSearchService.FindAll(searchSpan, searchStart.Value, this.SearchTerm, this.SearchOptions & ~FindOptions.Wrap)) + { + // As a safety measure, we don't include results of length zero in the navigator unless regular expressions are being used. + // Zero width matches could be useful in RegEx when for example somebody is trying to replace the start of the line using the "^" + // pattern. + if (result.Length == 0 && !regEx) + { + continue; + } + else + { + // We accept the first match + this.CurrentResult = result; + return true; + } + } + + if (forward) + { + ++index; + } + else + { + --index; + } + } + } + + // If nothing was found, then clear the current result + this.ClearCurrentResult(); + + return false; + } + + public bool Replace() + { + if (this.ReplaceTerm == null) + { + throw new InvalidOperationException("Can't replace with a null value. Set ReplaceTerm before performing a replace operation."); + } + + if (!this.CurrentResult.HasValue) + { + throw new InvalidOperationException("Need to have a current result before being able to replace. Perform a FindNext or FindPrevious operation first."); + } + + bool forward = (this.SearchOptions & FindOptions.SearchReverse) != FindOptions.SearchReverse; + bool regEx = (this.SearchOptions & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions; + + //This may not be the text buffer's current snapshot but that is the desired behavior. We're replacing the current result + //with the replace tuern. + SnapshotSpan result = this.CurrentResult.Value; + ITextSnapshot replaceSnapshot = result.Snapshot; + + SnapshotPoint searchStart = forward ? result.Start : result.End; + + SnapshotSpan searchSpan; + NormalizedSnapshotSpanCollection searchSpans = this.SearchSpans; + if ((searchSpans != null) && (searchSpans.Count > 0)) + { + //There could be a version skew here. + if (searchSpans[0].Snapshot != replaceSnapshot) + { + searchSpans = new NormalizedSnapshotSpanCollection(replaceSnapshot, TextSearchNavigator.TranslateTo(searchSpans[0].Snapshot, searchSpans, replaceSnapshot)); + } + + int index; + if (!TextSearchNavigator.TryGetIndexOfContainingSpan(searchSpans, searchStart, out index)) + { + // If the match is outside of the search range, then we should noop + return false; + } + searchSpan = searchSpans[index]; + } + else + { + searchSpan = new SnapshotSpan(replaceSnapshot, 0, replaceSnapshot.Length); + } + + searchSpan = forward ? new SnapshotSpan(searchStart, searchSpan.End) : new SnapshotSpan(searchSpan.Start, searchStart); + + //Ask the search engine to find the actual span we need to replace (& the corresponding replacement string). + string replacementValue = null; + SnapshotSpan? toReplace = _textSearchService.FindForReplace(searchSpan, this.SearchTerm, this.ReplaceTerm, this.SearchOptions, out replacementValue); + + if (toReplace.HasValue) + { + using (ITextEdit edit = _buffer.CreateEdit()) + { + Span replacementSpan = toReplace.Value.TranslateTo(edit.Snapshot, SpanTrackingMode.EdgeInclusive); + + if (!edit.Replace(replacementSpan, replacementValue)) + { + // The edit failed for some reason, perhaps read-only regions? + return false; + } + + edit.Apply(); + + if (edit.Canceled) + { + // The edit failed, most likely a handler of the changed event forced the edit to be canceled. + return false; + } + } + + return true; + } + + return false; + } + + public void ClearCurrentResult() + { + this.CurrentResult = null; + } + + /// <summary> + /// Translate start point to current snapshot + /// </summary> + private void UpdateStartPoint() + { + if (_startPoint.HasValue && _startPoint.Value.Snapshot != _buffer.CurrentSnapshot) + { + ITextVersion currentVersion = _startPoint.Value.Snapshot.Version; + ITextVersion targetVersion = _buffer.CurrentSnapshot.Version; + bool reverse = this.SearchOptions.HasFlag(Text.Operations.FindOptions.SearchReverse); + int currentStartPointPosition = _startPoint.Value.Position; + + Debug.Assert(targetVersion.VersionNumber >= currentVersion.VersionNumber, "We should never have to translate StartPoint into past"); + + while (currentVersion != targetVersion) + { + // Per INormalizedTextChangeCollection contract, there is not more than one change that we need to check. Find it with binary search + int changeCount = currentVersion.Changes.Count; + int lo = 0; + int hi = changeCount - 1; + + while (lo <= hi) + { + int mid = (lo + hi) / 2; + ITextChange textChange = currentVersion.Changes[mid]; + + if (currentStartPointPosition < textChange.OldPosition) + { + hi = mid - 1; + } + else if (currentStartPointPosition > textChange.OldEnd) + { + lo = mid + 1; + } + else + { + // Found the change. Let's adjust currentStartPointPosition + if (reverse) + { + // Partially verified by binary search. The full condition is in assert: + Debug.Assert(textChange.OldSpan.Start <= currentStartPointPosition && currentStartPointPosition <= textChange.OldSpan.End); + + if (currentStartPointPosition < textChange.OldSpan.End && !textChange.NewSpan.IsEmpty) + { + currentStartPointPosition = textChange.NewSpan.End - 1; + } + else // currentStartPosition == textChange.OldSpan.End + { + currentStartPointPosition = textChange.NewSpan.End; + } + } + else + { + if (textChange.OldSpan.End == currentStartPointPosition) + { + currentStartPointPosition = textChange.NewSpan.End; + } + else if (textChange.OldSpan.Start < currentStartPointPosition && !textChange.NewSpan.IsEmpty) + { + currentStartPointPosition = textChange.NewSpan.End - 1; + } + else + { + currentStartPointPosition = textChange.NewSpan.Start; + } + } + break; + } + } + + if (hi < lo) + { + Debug.Assert(hi == lo - 1, "If we haven't found a change, hi should be equal to lo - 1"); + + if (lo > 0) // Current position lies between the changes + { + ITextChange textChange = currentVersion.Changes[lo - 1]; + currentStartPointPosition = currentStartPointPosition + (textChange.NewEnd - textChange.OldEnd); + } + // else the start point lays prior to the first change (or there are no changes) and should remain intact + } + + currentVersion = currentVersion.Next; + } + + _startPoint = new SnapshotPoint(_buffer.CurrentSnapshot, currentStartPointPosition); + } + } + + static PointTrackingMode GetTrackingMode(bool isReverse) + { + return isReverse ? PointTrackingMode.Positive : PointTrackingMode.Negative; + } + #endregion + + #region ITextSearchNavigator2 Members + private NormalizedSnapshotSpanCollection _searchSpans; + public NormalizedSnapshotSpanCollection SearchSpans + { + get + { + var spans = _searchSpans; + if ((spans == null) && (_searchSpan != null)) + { + var snapshot = _buffer.CurrentSnapshot; + spans = new NormalizedSnapshotSpanCollection(new SnapshotSpan(snapshot, _searchSpan.GetSpan(snapshot))); + } + return spans; + } + + set + { + if (value != null) + { + if (value.Count == 0) + { + //Treat an empty collection as if it were null. + value = null; + } + else if (value[0].Snapshot.TextBuffer != _buffer) + { + throw new InvalidOperationException("The SearchSpan must be on the same buffer as the navigator itself."); + } + } + + _searchSpans = value; + _searchSpan = null; + } + } + #endregion + + private void AdvanceToSnapshot(ITextSnapshot snapshot) + { + if ((_searchSpans != null) && (_searchSpans.Count > 0) && (_searchSpans[0].Snapshot != snapshot)) + { + NormalizedSpanCollection newSpans = TextSearchNavigator.TranslateTo(_searchSpans[0].Snapshot, _searchSpans, snapshot); + this.SearchSpans = new NormalizedSnapshotSpanCollection(snapshot, newSpans); + } + } + + public static NormalizedSpanCollection TranslateTo(ITextSnapshot currentSnapshot, NormalizedSpanCollection currentSpans, ITextSnapshot targetSnapshot) + { + if ((currentSpans.Count == 0) || (currentSnapshot == targetSnapshot)) + { + return currentSpans; + } + + bool forwardInTime = (currentSnapshot.Version.VersionNumber < targetSnapshot.Version.VersionNumber); + + return new NormalizedSnapshotSpanCollection(targetSnapshot, + currentSpans.Select(s => + forwardInTime + ? Tracking.TrackSpanForwardInTime(SpanTrackingMode.EdgeInclusive, + s, + currentSnapshot.Version, targetSnapshot.Version) + : Tracking.TrackSpanBackwardInTime(SpanTrackingMode.EdgeInclusive, + s, + currentSnapshot.Version, targetSnapshot.Version)).Where(s => s.Length != 0)); + } + + /// <summary> + /// Search spans from [start ... spans.Count-1] for a span that contains point. + /// </summary> + private static bool TryGetIndexOfContainingSpan(NormalizedSpanCollection spans, int point, out int index) + { + int lo = 0; + int hi = spans.Count; + while (lo < hi) + { + int mid = (lo + hi) / 2; + Span s = spans[mid]; + + if (s.End < point) + { + lo = mid + 1; + } + else if (s.Start > point) + { + hi = mid; + } + else + { + //We know s.Start <= point <= s.End + index = mid; + return true; + } + } + + //None of the spans contains point. lo is the index of the span that follows point + index = lo; + return false; + } + } +} diff --git a/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs b/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs new file mode 100644 index 0000000..278532f --- /dev/null +++ b/src/Text/Impl/TextSearch/TextSearchNavigatorFactoryService.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Find.Implementation +{ + using System; + using System.ComponentModel.Composition; + + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Operations; + + [Export(typeof(ITextSearchNavigatorFactoryService))] + class TextSearchNavigatorFactoryService : ITextSearchNavigatorFactoryService + { + [Import] + ITextSearchService2 TextSearchService = null; + + #region ITextSearchNavigatorFactoryService Members + + public ITextSearchNavigator CreateSearchNavigator(ITextBuffer buffer) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + // Don't return a singleton since it's allowed to have multiple search navigators on the same buffer + return new TextSearchNavigator(this.TextSearchService, buffer); + } + + #endregion + } +} diff --git a/src/Text/Impl/TextSearch/TextSearchService.cs b/src/Text/Impl/TextSearch/TextSearchService.cs new file mode 100644 index 0000000..71c44e1 --- /dev/null +++ b/src/Text/Impl/TextSearch/TextSearchService.cs @@ -0,0 +1,751 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Find.Implementation +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.ComponentModel.Composition; + using System.Diagnostics; + using System.Text.RegularExpressions; + using Microsoft.VisualStudio.Text.Operations; + using Microsoft.VisualStudio.Text.Utilities; + using System.Linq; + using System.Threading; + + [Export(typeof(ITextSearchService))] + [Export(typeof(ITextSearchService2))] + // Ensure only a singleton instance is used for all operations + [PartCreationPolicy(System.ComponentModel.Composition.CreationPolicy.Shared)] + internal partial class TextSearchService : ITextSearchService2 + { + [Import] + ITextStructureNavigatorSelectorService _navigatorSelectorService; + + // Cache of recently used Regex expressions to save on construction + // of Regex objects. + static IDictionary<string, WeakReference> _cachedRegexEngines; + static ReaderWriterLockSlim _regexCacheLock; + + // Maximum number of Regex engines to cache + const int _maxCachedRegexEngines = 10; + + static TextSearchService() + { + _cachedRegexEngines = new Dictionary<string, WeakReference>(_maxCachedRegexEngines); + _regexCacheLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + } + + #region ITextSearchService Members + + public SnapshotSpan? FindNext(int startIndex, bool wraparound, FindData findData) + { + // We allow startIndex to be at the end of the buffer + if ((startIndex < 0) || (startIndex > findData.TextSnapshotToSearch.Length)) + { + throw new ArgumentOutOfRangeException("startIndex"); + } + + if (string.IsNullOrEmpty(findData.SearchString)) + { + throw new ArgumentException("Search pattern can't be empty or null", "findData"); + } + + FindOptions options = findData.FindOptions; + + if (wraparound) + { + options |= FindOptions.Wrap; + } + + if ((findData.FindOptions & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions) + { + // Validate regular expression + GetRegularExpressionMatches(options, findData.SearchString, string.Empty); + } + + bool wholeWord = (options & FindOptions.WholeWord) == FindOptions.WholeWord; + + // We want to wrap if either the boolean parameter is true or if the option is set via + // FindOptions + wraparound |= (findData.FindOptions & FindOptions.Wrap) == FindOptions.Wrap; + + ITextSnapshot snapshot = findData.TextSnapshotToSearch; + + // Remove whole word option, even if set so that new implementation's whole word logic is not used. + // Instead we use our own legacy whole word logic here to match the old behavior and preserve + // backward compatibility. + foreach (Tuple<SnapshotSpan, string> result in + FindAllForReplace(new SnapshotPoint(snapshot, startIndex), new SnapshotSpan(snapshot, Span.FromBounds(0, snapshot.Length)), + findData.SearchString, null, options & ~FindOptions.WholeWord)) + { + // In case whole word option was requested and the result is not a whole word, then + // keep looking. + if (wholeWord && !this.LegacyMatchesAWholeWord(result.Item1, findData)) + { + continue; + } + + return result.Item1; + } + + return null; + } + + public Collection<SnapshotSpan> FindAll(FindData findData) + { + if (string.IsNullOrEmpty(findData.SearchString)) + { + throw new ArgumentException("Search pattern can't be empty or null", "findData"); + } + + FindOptions options = findData.FindOptions; + + bool wholeWord = (options & FindOptions.WholeWord) == FindOptions.WholeWord; + + ITextSnapshot snapshot = findData.TextSnapshotToSearch; + SnapshotSpan searchRange = new SnapshotSpan(snapshot, Span.FromBounds(0, snapshot.Length)); + + Collection<SnapshotSpan> results = new Collection<SnapshotSpan>(); + + // Remove whole word option, even if set so that new implementation's whole word logic is not used. + // Instead we use our own legacy whole word logic here to match the old behavior and preserve + // backward compatibility. + foreach (SnapshotSpan result in this.FindAll(searchRange, findData.SearchString, options & ~FindOptions.WholeWord)) + { + // In case whole word option was requested and the result is not a whole word, then + // keep looking. + if (wholeWord && !this.LegacyMatchesAWholeWord(result, findData)) + { + continue; + } + + results.Add(result); + } + + return results; + } + + #endregion + + #region ITextSearchService2 Members + + public SnapshotSpan? Find(SnapshotPoint startingPosition, string searchPattern, FindOptions options) + { + if (string.IsNullOrEmpty(searchPattern)) + { + throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + } + + return Find(startingPosition, new SnapshotSpan(startingPosition.Snapshot, Span.FromBounds(0, startingPosition.Snapshot.Length)), searchPattern, options); + } + + public SnapshotSpan? Find(SnapshotSpan searchRange, SnapshotPoint startingPosition, string searchPattern, FindOptions options) + { + if (string.IsNullOrEmpty(searchPattern)) + { + throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + } + + if (searchRange.Snapshot != startingPosition.Snapshot) + { + throw new ArgumentException("The search range and search starting position must belong to the same snapshot."); + } + + if (!ContainedBySpan(searchRange, startingPosition)) + { + throw new ArgumentException("The search start point must be contained by the search range."); + } + + return Find(startingPosition, searchRange, searchPattern, options); + } + + public SnapshotSpan? FindForReplace(SnapshotPoint startingPosition, string searchPattern, string replacePattern, FindOptions options, out string expandedReplacePattern) + { + if (string.IsNullOrEmpty(searchPattern)) + { + throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + } + + if (replacePattern == null) + { + throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern"); + } + + return FindForReplace(startingPosition, new SnapshotSpan(startingPosition.Snapshot, Span.FromBounds(0, startingPosition.Snapshot.Length)), + searchPattern, replacePattern, options, out expandedReplacePattern); + } + + public SnapshotSpan? FindForReplace(SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options, out string expandedReplacePattern) + { + if (string.IsNullOrEmpty(searchPattern)) + { + throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + } + + if (replacePattern == null) + { + throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern"); + } + + return FindForReplace(((options & FindOptions.SearchReverse) != FindOptions.SearchReverse) ? searchRange.Start : searchRange.End, searchRange, searchPattern, replacePattern, options, out expandedReplacePattern); + } + + public IEnumerable<SnapshotSpan> FindAll(SnapshotSpan searchRange, string searchPattern, FindOptions options) + { + if (string.IsNullOrEmpty(searchPattern)) + { + throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + } + + if (searchRange.Length == 0) + { + return new SnapshotSpan[] { }; + } + + return FindAllForReplace(searchRange.Start, searchRange, searchPattern, null, options).Select(r => r.Item1); + } + + public IEnumerable<SnapshotSpan> FindAll(SnapshotSpan searchRange, SnapshotPoint startingPosition, string searchPattern, FindOptions options) + { + if (string.IsNullOrEmpty(searchPattern)) + { + throw new ArgumentException("Pattern can't be empty or null", "searchPattern"); + } + + if (searchRange.Length == 0) + { + return new SnapshotSpan[] { }; + } + + if (searchRange.Snapshot != startingPosition.Snapshot) + { + throw new ArgumentException("searchRange and startingPosition parameters must belong to the same snapshot."); + } + + if (!ContainedBySpan(searchRange, startingPosition)) + { + throw new InvalidOperationException("Can't perform a search when the startingPosition is not contained by the searchRange."); + } + + return FindAllForReplace(startingPosition, searchRange, searchPattern, null, options).Select(r => r.Item1); + } + + public IEnumerable<Tuple<SnapshotSpan, string>> FindAllForReplace(SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options) + { + if (string.IsNullOrEmpty(searchPattern)) + { + throw new ArgumentException("Search pattern can't be null or empty.", "searchPattern"); + } + + if (replacePattern == null) + { + throw new ArgumentNullException("Replace pattern can't be null.", "replacePattern"); + } + + return FindAllForReplace(searchRange.Start, searchRange, searchPattern, replacePattern, options); + } + + #endregion + + #region Private Helpers + + private static SnapshotSpan? Find(SnapshotPoint startPosition, SnapshotSpan searchRange, string searchPattern, FindOptions options) + { + // Perform a find all and return the first result if any is available. Note that find all is an enumerator and won't search the + // entire range unless it is necessary. + Tuple<SnapshotSpan, string> match = FindAllForReplace(startPosition, searchRange, searchPattern, null, options).FirstOrDefault(); + + if (match != null) + { + return match.Item1; + } + + return null; + } + + private static SnapshotSpan? FindForReplace(SnapshotPoint startPosition, SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options, out string expandedReplacePattern) + { + // Set this value to empty to adhere to the contract (i.e. when no matches are found, this value should be the empty string) + expandedReplacePattern = string.Empty; + + // Perform a find all for replace over the range of interest. Note that this operation is lazy and will stop when the first result is + // found. + Tuple<SnapshotSpan, string> match = FindAllForReplace(startPosition, searchRange, searchPattern, replacePattern, options).FirstOrDefault(); + + if (match != null) + { + expandedReplacePattern = match.Item2; + return match.Item1; + } + + return null; + } + + private static IEnumerable<Tuple<SnapshotSpan, string>> FindAllForReplace(SnapshotPoint startPosition, SnapshotSpan searchRange, string searchPattern, string replacePattern, FindOptions options) + { + bool multiLine = (options & FindOptions.Multiline) == FindOptions.Multiline; + bool wholeWord = (options & FindOptions.WholeWord) == FindOptions.WholeWord; + + IEnumerable<Tuple<SnapshotSpan, string>> searchResults = null; + + // Perform the search depending on whether we are forced to perform multi-line search + if (multiLine) + { + searchResults = FindMultiline(startPosition, searchRange, options, searchPattern, replacePattern); + } + else + { + searchResults = FindSingleLine(startPosition, searchRange, options, searchPattern, replacePattern); + } + + foreach (Tuple<SnapshotSpan, string> searchResult in searchResults) + { + // Don't accept the result if whole word option is selected and the result + // is not a whole word + if (wholeWord && !IsWholeWord(searchResult.Item1)) + { + continue; + } + + // found a suitable match, return it! + yield return searchResult; + } + } + + private static IEnumerable<Tuple<SnapshotSpan, string>> FindMultiline(SnapshotPoint startPosition, SnapshotSpan searchRange, FindOptions options, string searchPattern, string replacePattern = null) + { + Debug.Assert(searchRange.Contains(startPosition) || searchRange.End == startPosition); + + // This is pretty disgusting since we have to search including line endings + // we have to look at the entire string. There can be optimizations done for specific cases + // where one can count the number of line endings that a search pattern could at most include + // and then search chunks with that many line endings at a time, but for patterns where the quantifier + // is * this can't be done; e.g. a.*[\n]*.*b + string rangeText = searchRange.GetText(); + + bool wrap = (options & FindOptions.Wrap) == FindOptions.Wrap; + + foreach (var result in FindInString(searchRange.Start, startPosition - searchRange.Start, rangeText, options, searchPattern, replacePattern)) + { + yield return result; + } + + if (wrap) + { + bool reverse = (options & FindOptions.SearchReverse) == FindOptions.SearchReverse; + + foreach (var result in FindInString(searchRange.Start, reverse ? rangeText.Length : 0, rangeText, options, searchPattern, replacePattern)) + { + // Since we are wrapping, check the validity of the result to ensure it was not returned + // in our first search above + if (reverse) + { + if (result.Item1.End <= startPosition) + { + break; + } + } + else + { + if (result.Item1.Start >= startPosition) + { + break; + } + } + + yield return result; + } + } + } + + private static IEnumerable<Tuple<SnapshotSpan, string>> FindSingleLine(SnapshotPoint startPosition, SnapshotSpan searchRange, FindOptions options, string searchPattern, string replacePattern = null) + { + Debug.Assert(searchRange.Contains(startPosition) || searchRange.End == startPosition); + + bool reverse = (options & FindOptions.SearchReverse) == FindOptions.SearchReverse; + bool regEx = (options & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions; + bool wrap = (options & FindOptions.Wrap) == FindOptions.Wrap; + + ITextSnapshot snapshot = startPosition.Snapshot; + ITextSnapshotLine startLine = startPosition.GetContainingLine(); + int lineIncrement = reverse ? -1 : 1; + int startLineNumber = startLine.LineNumber; + int searchLineStart = searchRange.Start.GetContainingLine().LineNumber; + int searchLineEnd = searchRange.End.GetContainingLine().LineNumber; + + SnapshotSpan firstLineSpan = startLine.ExtentIncludingLineBreak.Intersection(searchRange).Value; + + // Find results in "first part" of the first line, then search the rest of the range. If wrap is on, afterwards wrap and search + // the rest of the range, then finally check the first line again and search its "second part". + + // We are effecitvely splitting the first line into the following two portions: + // + // FirstLineStart --------------------- startPosition --------------------- FirstLineEnd + // + // Depending on the direction of the search, one of the pieces will be searched first, and the other will be searched if + // wrap is on towards the bottom of the algorithm after all other lines have been searched. + + // Examine first line (regEx can match 0 length strings so we can't exclude that case). + if ((firstLineSpan.Length > 0) || regEx) + { + // Regular expression searches start from the index passed - 1 in reverse direction, as such, if we are at a line boundary + // we need to be examining the line before + bool skip = regEx && reverse && startPosition != 0 && startLine.Start == startPosition; + + if (!skip) + { + foreach (var result in FindInString(firstLineSpan.Start, startPosition - firstLineSpan.Start, firstLineSpan.GetText(), options, searchPattern, replacePattern)) + { + yield return result; + } + } + } + + // Examine the rest of the range in the original direction + for (int i = startLineNumber + lineIncrement; (i >= searchLineStart && i <= searchLineEnd); i += lineIncrement) + { + foreach (var result in FindInLine(i, searchRange, options, searchPattern, replacePattern)) + { + yield return result; + } + } + + // If wrap is enabled, now wrap around the search boundary and continue the search + if (wrap) + { + for (int i = reverse ? searchLineEnd : searchLineStart; i != startLineNumber; i += lineIncrement) + { + foreach (var result in FindInLine(i, searchRange, options, searchPattern, replacePattern)) + { + yield return result; + } + } + + // Finally look at the remainder of the first line + foreach (var result in FindInString(firstLineSpan.Start, reverse ? firstLineSpan.End - firstLineSpan.Start : 0, + firstLineSpan.GetText(), options, searchPattern, replacePattern)) + { + // Since we are wrapping, check the validity of the result to ensure it was not returned + // in our first search of the first line before wrapping + if (reverse) + { + if (result.Item1.End.Position <= startPosition.Position) + { + break; + } + } + else + { + if (result.Item1.Start >= startPosition) + { + break; + } + } + + yield return result; + } + } + } + + private static IEnumerable<Tuple<SnapshotSpan, string>> FindInLine(int lineNumber, SnapshotSpan searchRange, FindOptions options, string searchPattern, string replacePattern) + { + bool reverse = (options & FindOptions.SearchReverse) == FindOptions.SearchReverse; + ITextSnapshotLine line = searchRange.Snapshot.GetLineFromLineNumber(lineNumber); + SnapshotSpan? searchSpan = searchRange.Intersection(line.ExtentIncludingLineBreak); + + if (searchSpan.HasValue) + { + foreach (var result in FindInString(searchSpan.Value.Start, reverse ? searchSpan.Value.End - searchSpan.Value.Start : 0, searchSpan.Value.GetText(), options, searchPattern, replacePattern)) + { + yield return result; + } + } + } + + private static bool IsWholeWord(SnapshotSpan result) + { + if (result.Length < 1) + { + return false; + } + + return UnicodeWordExtent.IsWholeWord(result); + } + + private static bool ContainedBySpan(SnapshotSpan searchRange, SnapshotPoint startingPosition) + { + // We make an exception for spans to contain points if the end point of the span matches the startingPosition. + // For backwards searches, this allows the consumer to find a point at the very end of the searchRange, for + // forward searches, this has no effect. + return searchRange.Contains(startingPosition) || searchRange.End == startingPosition; + } + + private static StringComparison GetStringComparison(FindOptions findOptions) + { + bool matchCase = ((findOptions & FindOptions.MatchCase) == FindOptions.MatchCase); + bool cultureSensitive = ((findOptions & FindOptions.OrdinalComparison) != FindOptions.OrdinalComparison); + + if (cultureSensitive) + { + if (matchCase) + { + return StringComparison.CurrentCulture; + } + else + { + return StringComparison.CurrentCultureIgnoreCase; + } + } + else + { + if (matchCase) + { + return StringComparison.Ordinal; + } + else + { + return StringComparison.OrdinalIgnoreCase; + } + } + } + + /// <summary> + /// Returns collection of matches of a regular expression search over the provided input string. + /// </summary> + private static MatchCollection GetRegularExpressionMatches(FindOptions options, string searchTerm, string toSearch, int startingIndex = -1) + { + RegexOptions regExOptions = GetRegexOptions(options); + + try + { + Regex engine = GetOrCreateCachedRegex(regExOptions, searchTerm); + + if (startingIndex == -1) + { + return engine.Matches(toSearch); + } + else + { + return engine.Matches(toSearch, startingIndex); + } + } + catch (ArgumentException e) + { + throw new ArgumentException("Invalid regular expression", e); + } + } + + private static Regex GetOrCreateCachedRegex(RegexOptions options, string searchTerm) + { + // Try to reduce the cache entries if need be + ScavengeRegexCache(); + + string key = GetRegexKey(options, searchTerm); + Regex result = null; + + try + { + _regexCacheLock.EnterReadLock(); + + WeakReference regexReference = null; + if (_cachedRegexEngines.TryGetValue(key, out regexReference) && regexReference.IsAlive) + { + result = regexReference.Target as Regex; + } + } + finally + { + _regexCacheLock.ExitReadLock(); + } + + if (result == null) + { + // Either the key was not found, or it was found and the reference is dead + result = new Regex(searchTerm, options); + + try + { + _regexCacheLock.EnterWriteLock(); + + // Cache the value + _cachedRegexEngines[key] = new WeakReference(result); + } + finally + { + _regexCacheLock.ExitWriteLock(); + } + } + + return result; + } + + private static void ScavengeRegexCache() + { + // If we have reached our maximum capacity, do a pass and kill inactive references + if (_maxCachedRegexEngines == _cachedRegexEngines.Count) + { + try + { + _regexCacheLock.EnterWriteLock(); + + // Keep a list of stale keys + string[] staleKeys = new string[_maxCachedRegexEngines]; + + int staleKeyIterator = 0; + + // Record all stale keys + foreach (var keyValuePair in _cachedRegexEngines) + { + if (!keyValuePair.Value.IsAlive) + { + staleKeys[staleKeyIterator++] = keyValuePair.Key; + } + } + + // Remove stale keys + for (int i = 0; i < staleKeyIterator; i++) + { + _cachedRegexEngines.Remove(staleKeys[i]); + } + } + finally + { + _regexCacheLock.ExitWriteLock(); + } + } + } + + /// <summary> + /// Creates a unique key for the combination of regular expression options and the search term used. Logic + /// is copied from ndp/fx/src/Regex/System/Text/RegularExpressions/Regex.cs + /// </summary> + private static string GetRegexKey(RegexOptions options, string searchTerm) + { + string cultureKey = null; + + // Try to look up this regex in the cache. We do this regardless of whether useCache is true since there's + // really no reason not to. + if ((options & RegexOptions.CultureInvariant) != 0) + cultureKey = System.Globalization.CultureInfo.InvariantCulture.ToString(); // "English (United States)" + else + cultureKey = System.Globalization.CultureInfo.CurrentCulture.ToString(); + + return ((int)options).ToString(System.Globalization.NumberFormatInfo.InvariantInfo) + ":" + cultureKey + ":" + searchTerm; + } + + /// <summary> + /// Converts regular find options to regular expression options. + /// </summary> + private static RegexOptions GetRegexOptions(FindOptions options) + { + RegexOptions regExOptions = RegexOptions.None; + + if ((options & FindOptions.Multiline) == FindOptions.Multiline) + { + regExOptions |= RegexOptions.Multiline; + } + + if ((options & FindOptions.SingleLine) == FindOptions.SingleLine) + { + regExOptions |= RegexOptions.Singleline; + } + + if ((options & FindOptions.MatchCase) != FindOptions.MatchCase) + { + regExOptions |= RegexOptions.IgnoreCase; + } + + if ((options & FindOptions.SearchReverse) == FindOptions.SearchReverse) + { + regExOptions |= RegexOptions.RightToLeft; + } + + return regExOptions; + } + + /// <summary> + /// Determines whether the given match qualifies as a whole word using <see cref="ITextStructureNavigator"/>. + /// </summary> + /// <remarks> + /// In the new implementation, we don't use any external parties to determine word boundaries, rather we pick a small + /// predictable range of characters and use them as word splitters. The primary reason for this is performance as calls + /// to external components could be slow. Further, we want the search service to be a dumb but very predictable and simple + /// text based search service so we enforce our own rules in the new implementation. + /// </remarks> + private bool LegacyMatchesAWholeWord(SnapshotSpan result, FindData findData) + { + ITextStructureNavigator textStructureNavigator = findData.TextStructureNavigator; + + if (textStructureNavigator == null) + { + // the navigator was never set; create one without benefit of context + textStructureNavigator = _navigatorSelectorService.GetTextStructureNavigator(findData.TextSnapshotToSearch.TextBuffer); + } + + // We'll need to get the left extent and right extent in case the match spans across multiple words + Microsoft.VisualStudio.Text.Operations.TextExtent leftExtent = textStructureNavigator.GetExtentOfWord(result.Start); + Microsoft.VisualStudio.Text.Operations.TextExtent rightExtent = textStructureNavigator.GetExtentOfWord(result.Length > 0 ? result.End - 1 : result.End); + + return (result.Start == leftExtent.Span.Start) && (result.End == rightExtent.Span.End); + } + + private static IEnumerable<Tuple<SnapshotSpan, string>> FindInString(SnapshotPoint snapshotOffset, int searchStartIndex, string textData, FindOptions options, string searchTerm, string replaceTerm = null) + { + bool reverse = (options & FindOptions.SearchReverse) == FindOptions.SearchReverse; + bool regEx = (options & FindOptions.UseRegularExpressions) == FindOptions.UseRegularExpressions; + + if (regEx) + { + // Perform search with regular expressions + foreach (Match match in GetRegularExpressionMatches(options, searchTerm, textData, searchStartIndex)) + { + SnapshotSpan matchSpan = new SnapshotSpan(snapshotOffset.Snapshot, new Span(snapshotOffset + match.Index, match.Length)); + string replacementValue = replaceTerm != null ? match.Result(replaceTerm) : null; + + yield return Tuple.Create<SnapshotSpan, string>(matchSpan, replacementValue); + } + } + else + { + // Perform raw string search + while ((reverse && searchStartIndex > 0) || (!reverse && searchStartIndex < textData.Length)) + { + // Look for the next match + // Note: LastIndexOf performs a search inclusive of the position passed to it, but we want reverse searches + // to be exclusive of the start position. + int matchIndex = reverse ? + textData.LastIndexOf(searchTerm, searchStartIndex - 1, GetStringComparison(options)) : + textData.IndexOf(searchTerm, searchStartIndex, GetStringComparison(options)); + + if (matchIndex == -1) + { + // Couldn't find any more results + yield break; + } + + SnapshotSpan potentialResult = new SnapshotSpan(snapshotOffset.Snapshot, snapshotOffset + matchIndex, searchTerm.Length); + + yield return Tuple.Create<SnapshotSpan, string>(potentialResult, replaceTerm); + + // Prepare for next search + // + // If the search is forward, we want to continue one character next to the start of the previous result, for instance, consider + // searching for "aa" in "aaa". + // If the search is backward, we want to continue one character prior to the end of the previous result, again, consider searching + // for "aa" in "aaa" from the end of the buffer. Notice that even though the result span does not include its .End position, we + // subtract only one, because the search performed above is not inclusive of the start index for reverse direction + searchStartIndex = (reverse ? potentialResult.End.Position - 1 : potentialResult.Start.Position + 1) - snapshotOffset.Position; + } + } + } + + #endregion + } +} diff --git a/src/Text/Impl/TextSearch/TextSearchTagger.cs b/src/Text/Impl/TextSearch/TextSearchTagger.cs new file mode 100644 index 0000000..6a8852b --- /dev/null +++ b/src/Text/Impl/TextSearch/TextSearchTagger.cs @@ -0,0 +1,281 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Find.Implementation +{ + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Operations; + using Microsoft.VisualStudio.Text.Tagging; + using System; + using System.Collections.Generic; + + /// <summary> + /// A general tagger that takes a search term and tags all matching occurences of it. + /// </summary> + /// <remarks> + /// This tagger -- like most others -- will not raise a TagsChanged event when the buffer changes. + /// </remarks> + class TextSearchTagger<T> : ITextSearchTagger<T> where T : ITag + { + // search service to use for doing the real search + ITextSearchService2 _searchService; + + // list of items to search for and tag + internal IList<BackgroundSearch<T>> _searchTerms = new List<BackgroundSearch<T>>(); + + // buffer over which search is being performed + ITextBuffer _buffer; + + public TextSearchTagger(ITextSearchService2 searchService, ITextBuffer buffer) + { + _searchService = searchService; + _buffer = buffer; + } + + #region Private Helpers + + private void InvalidateTags(SnapshotSpan span) + { + EventHandler<SnapshotSpanEventArgs> tagsChangedListeners = this.TagsChanged; + + if (tagsChangedListeners != null) + { + tagsChangedListeners.Invoke(this, new SnapshotSpanEventArgs(span)); + } + } + + private void InvalidateTags() + { + this.InvalidateTags(new SnapshotSpan(_buffer.CurrentSnapshot, 0, _buffer.CurrentSnapshot.Length)); + } + + internal void ResultsCalculated(ITextSnapshot snapshot, NormalizedSpanCollection spans) + { + if (spans.Count > 0) + { + SnapshotSpan changedSpan = new SnapshotSpan(snapshot, Span.FromBounds(spans[0].Start, spans[spans.Count - 1].End)); + this.InvalidateTags(changedSpan); + } + } + + #endregion + + #region ITextSearchTagger<T> Members + + private NormalizedSnapshotSpanCollection _searchSpans; + public NormalizedSnapshotSpanCollection SearchSpans + { + get + { + return _searchSpans; + } + set + { + if (value != null) + { + if (value.Count == 0) + { + //Treat an empty collection as if it were null. + value = null; + } + else if (value[0].Snapshot.TextBuffer != _buffer) + { + throw new ArgumentException("The provided SearchSpan value must belong to the same buffer as the tagger itself."); + } + } + + if (value == _searchSpans) + { + return; + } + + _searchSpans = value; + + this.InvalidateTags(); + } + } + + public void TagTerm(string searchTerm, FindOptions searchOptions, Func<SnapshotSpan, T> tagFactory) + { + if ((searchOptions & FindOptions.SearchReverse) == FindOptions.SearchReverse) + { + throw new ArgumentException("FindOptions.SearchReverse is invalid as searches are performed forwards to ensure all matches in a requested search span are found.", "searchOptions"); + } + + if ((searchOptions & FindOptions.Wrap) == FindOptions.Wrap) + { + throw new ArgumentException("FindOptions.Wrap is invalid as searches are performed forwards with no wrapping to ensure all matches in a requested span are found.", "searchOptions"); + } + + _searchTerms.Add(new BackgroundSearch<T>(_searchService, _buffer, searchTerm, searchOptions, tagFactory, this.ResultsCalculated)); + + this.InvalidateTags(); + } + + public void ClearTags() + { + if (_searchTerms.Count == 0) + { + return; + } + + ITextSnapshot snapshot = _buffer.CurrentSnapshot; + int start = int.MaxValue; + int end = int.MinValue; + + foreach (BackgroundSearch<T> search in _searchTerms) + { + // Abort any ongoing background search operation since we no longer are interested in the results + NormalizedSnapshotSpanCollection results = search.Results; + + if ((results != null) && (results.Count > 0)) + { + int s = results[0].Start.TranslateTo(snapshot, PointTrackingMode.Negative); + if (s < start) + start = s; + + int e = results[results.Count - 1].End.TranslateTo(snapshot, PointTrackingMode.Positive); + if (e > end) + end = e; + } + + search.Dispose(); + } + + // Clear all currently tagging search terms + _searchTerms.Clear(); + + // Notify listeners of changed tags over the span where we had any results. + if (start < end) + this.InvalidateTags(new SnapshotSpan(snapshot, start, end - start)); + } + + #endregion + + #region ITagger<T> Members + + public IEnumerable<ITagSpan<T>> GetTags(NormalizedSnapshotSpanCollection requestedSpans) + { + //We should always be called with a non-empty span. + if (requestedSpans != null && requestedSpans.Count > 0) + { + ITextSnapshot searchSnapshot = _buffer.CurrentSnapshot; + requestedSpans = new NormalizedSnapshotSpanCollection(searchSnapshot, TextSearchNavigator.TranslateTo(requestedSpans[0].Snapshot, requestedSpans, searchSnapshot)); + + if ((_searchSpans != null) && (_searchSpans.Count > 0)) + { + //The search has been narrowed via _searchSpan ... limit the request to the search range (after making sure it is on the correct snapshot). + if (_searchSpans[0].Snapshot != searchSnapshot) + { + NormalizedSpanCollection newSpans = TextSearchNavigator.TranslateTo(_searchSpans[0].Snapshot, _searchSpans, searchSnapshot); + _searchSpans = new NormalizedSnapshotSpanCollection(searchSnapshot, newSpans); + } + + requestedSpans = new NormalizedSnapshotSpanCollection(searchSnapshot, NormalizedSpanCollection.Intersection(requestedSpans, _searchSpans)); + + if (requestedSpans.Count == 0) + { + yield break; + } + } + + foreach (var search in _searchTerms) + { + //Queue up a search if we need one. + search.QueueSearch(requestedSpans); + + //Report any results from the search (if we've got them) + var results = search.Results; + if (results.Count > 0) + { + //Results could be on an old snapshot (and, if so, a new search has already been queued up) but we need to get the results on the current snapshot. + if (results[0].Snapshot != searchSnapshot) + { + results = new NormalizedSnapshotSpanCollection(searchSnapshot, TextSearchNavigator.TranslateTo(results[0].Snapshot, results, searchSnapshot)); + } + + if (_searchSpans != null) + { + results = new NormalizedSnapshotSpanCollection(searchSnapshot, NormalizedSpanCollection.Intersection(results, _searchSpans)); + } + + int start = 0; + foreach (var span in requestedSpans) + { + start = TextSearchTagger<T>.IndexOfContainingSpan(results, span.Start, start, false); + if (start >= results.Count) + break; //All done. + + int end = TextSearchTagger<T>.IndexOfContainingSpan(results, span.End, start, true); + + while (start < end) + { + T tag = search.TagFactory.Invoke(results[start]); + + if (tag != null) + { + yield return new TagSpan<T>(results[start], tag); + } + + start++; + } + } + } + } + } + } + + /// <summary> + /// Search spans from [start ... spans.Count-1] for a span that contains point. + /// If a span does contain point, return the index + 1 + /// If a span does not contain point, return the index of the first span that starts after point (or spans.Count if there are none). + /// </summary> + private static int IndexOfContainingSpan(NormalizedSpanCollection spans, int point, int start, bool isEndPoint) + { + int lo = start; + int hi = spans.Count; + while (lo < hi) + { + int mid = (lo + hi) / 2; + Span s = spans[mid]; + + if (s.End < point) + { + lo = mid + 1; + } + else if (s.Start > point) + { + hi = mid; + } + else + { + //We know s.Start <= point <= s.End + // + //If point is an endPoint + // we want to return mid + 1 if a span ending at point overlaps s (== point != s.Start). Otherwise return mid. + // + //If point is a startPoint + // we want to return mid if a span starting at point overlaps s (== point == s.End). Otherwise return mid + 1. + if (isEndPoint) + { + return (point != s.Start) ? (mid + 1) : mid; + } + else + { + return (point == s.End) ? (mid + 1) : mid; + } + } + } + + return lo; + } + + public event EventHandler<SnapshotSpanEventArgs> TagsChanged; + + #endregion + } +} diff --git a/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs b/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs new file mode 100644 index 0000000..fca8282 --- /dev/null +++ b/src/Text/Impl/TextSearch/TextSearchTaggerFactoryService.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +// This file contain implementations details that are subject to change without notice. +// Use at your own risk. +// +namespace Microsoft.VisualStudio.Text.Find.Implementation +{ + using System; + using System.ComponentModel.Composition; + + using Microsoft.VisualStudio.Text.Operations; + using Microsoft.VisualStudio.Text.Tagging; + + [Export(typeof(ITextSearchTaggerFactoryService))] + class TextSearchTaggerFactoryService : ITextSearchTaggerFactoryService + { + [Import] + private ITextSearchService2 TextSearchService = null; + + #region ITextSearchTaggerFactoryService Members + + public ITextSearchTagger<T> CreateTextSearchTagger<T>(ITextBuffer buffer) where T : ITag + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + + // Don't return singleton instances since multiple taggers can exist per buffer + return new TextSearchTagger<T>(this.TextSearchService, buffer); + } + + #endregion + } +} diff --git a/src/Text/Util/TextDataUtil/ArgumentValidation.cs b/src/Text/Util/TextDataUtil/ArgumentValidation.cs new file mode 100644 index 0000000..69ab9ef --- /dev/null +++ b/src/Text/Util/TextDataUtil/ArgumentValidation.cs @@ -0,0 +1,23 @@ +// +// 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.Diagnostics.Contracts; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + internal static class ArgumentValidation + { + [ContractArgumentValidator] + public static void NotNull(object variable, string variableName) + { + if (variable == null) + { + throw new ArgumentNullException(variableName); + } + + Contract.EndContractBlock(); + } + } +} diff --git a/src/Text/Util/TextDataUtil/BufferTracker.cs b/src/Text/Util/TextDataUtil/BufferTracker.cs new file mode 100644 index 0000000..f64e6bc --- /dev/null +++ b/src/Text/Util/TextDataUtil/BufferTracker.cs @@ -0,0 +1,151 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Projection; + using Microsoft.VisualStudio.Utilities; + + internal sealed class BufferTracker : IDisposable + { + private static List<WeakReference> allocatedBuffers = new List<WeakReference>(); + + private ITextBufferFactoryService textBufferFactoryService; + private IProjectionBufferFactoryService projectionBufferFactoryService; + + public BufferTracker(ITextBufferFactoryService textBufferFactoryService, + IProjectionBufferFactoryService projectionBufferFactoryService) + { + this.textBufferFactoryService = textBufferFactoryService; + this.projectionBufferFactoryService = projectionBufferFactoryService; + textBufferFactoryService.TextBufferCreated += OnBufferCreated; + projectionBufferFactoryService.ProjectionBufferCreated += OnBufferCreated; + } + + public void Dispose() + { + this.textBufferFactoryService.TextBufferCreated -= OnBufferCreated; + this.projectionBufferFactoryService.ProjectionBufferCreated -= OnBufferCreated; + FreeSnapshotTrackers(); + allocatedBuffers.Clear(); + } + + private static void FreeSnapshotTrackers() + { + for (int b = 0; b < allocatedBuffers.Count; ++b) + { + ITextBuffer buffer = allocatedBuffers[b] as ITextBuffer; + if (buffer != null) + { + SnapshotTracker st; + if (buffer.Properties.TryGetProperty<SnapshotTracker>(typeof(SnapshotTracker), out st)) + { + st.Dispose(); + buffer.Properties.RemoveProperty(typeof(SnapshotTracker)); + } + } + } + } + + public static void TagBuffer(ITextBuffer buffer, string tag) + { + TextUtilities.TagBuffer(buffer, tag); + } + + public static string GetTag(ITextBuffer buffer) + { + return TextUtilities.GetTag(buffer); + } + + public static int ReportLiveBuffers(System.IO.TextWriter writer) + { + int totalBuffers = 0; + if (allocatedBuffers != null) + { + for (int e = 0; e < allocatedBuffers.Count; ++e) + { + if (allocatedBuffers[e].IsAlive) + { + ++totalBuffers; + } + } + + if (writer != null) + { + writer.Write + (String.Format + (System.Globalization.CultureInfo.CurrentCulture, + "{0}\r\n", totalBuffers)); + + int liveBuffers = 0; + for (int b = 0; b < allocatedBuffers.Count; ++b) + { + ITextBuffer buffer = allocatedBuffers[b].Target as ITextBuffer; + if (buffer != null) + { + ++liveBuffers; + + string tag = GetTag(buffer); + writer.Write + (String.Format + (System.Globalization.CultureInfo.CurrentCulture, + "{0,5} {1}\r\n", b, !String.IsNullOrEmpty(tag) ? tag : "Untagged")); + + SnapshotTracker st; + if (buffer.Properties.TryGetProperty<SnapshotTracker>(typeof(SnapshotTracker), out st)) + { + st.ReportLiveSnapshots(writer); + } + + foreach (KeyValuePair<Object, Object> pair in ((IPropertyOwner)buffer).Properties.PropertyList) + { + if (pair.Key.ToString() != "tag") + { + string rhsType; + if (pair.Value == null) + { + rhsType = "?null"; + } + else + { + rhsType = pair.Value.GetType().Name; + if (pair.Value is WeakReference) + { + Object target = (pair.Value as WeakReference).Target; + if (target != null) + { + rhsType = rhsType + "(" + target.GetType().Name + ")"; + } + } + } + writer.Write(String.Format(System.Globalization.CultureInfo.CurrentCulture, " {0}\r\n", rhsType)); + } + } + } + } + } + } + + return totalBuffers; + } + + private void OnBufferCreated(object sender, TextBufferCreatedEventArgs e) + { + // look for a hole in allocatedBuffers. This is linear in number of extant buffers + // but we won't take this path except in diagnostic conditions. + for (int b = 0; b < allocatedBuffers.Count; ++b) + { + if (!allocatedBuffers[b].IsAlive) + { + allocatedBuffers[b].Target = e.TextBuffer; + return; + } + } + allocatedBuffers.Add(new WeakReference(e.TextBuffer)); + e.TextBuffer.Properties.AddProperty(typeof(SnapshotTracker), new SnapshotTracker(e.TextBuffer)); + } + } +}
\ No newline at end of file diff --git a/src/Text/Util/TextDataUtil/DifferenceCollection.cs b/src/Text/Util/TextDataUtil/DifferenceCollection.cs new file mode 100644 index 0000000..cc2ef6b --- /dev/null +++ b/src/Text/Util/TextDataUtil/DifferenceCollection.cs @@ -0,0 +1,201 @@ +// +// 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; + +namespace Microsoft.VisualStudio.Text.Differencing.Implementation +{ + /// <summary> + /// Represents a collection of differences over two lists of same-typed elements, + /// given a "maximal" match sequence (as generated from a difference algorithm). + /// You can enumerate over the Differences in this collection. + /// </summary> + /// <typeparam name="T">The element type of the compared lists</typeparam> + internal class DifferenceCollection<T> : IDifferenceCollection<T> + { + private IEnumerable<Tuple<int, int>> sequence; + private IList<T> originalLeft; + private IList<T> originalRight; + + private IList<Difference> diffs; + + public DifferenceCollection(IList<Difference> diffs, IList<T> originalLeft, IList<T> originalRight) + { + this.originalLeft = originalLeft; + this.originalRight = originalRight; + + this.diffs = diffs; + this.sequence = new MatchEnumerator(diffs, originalLeft.Count); + } + + public static Match CreateInitialMatch(int originalStart) + { + return (originalStart != 0) ? (new Match(new Span(0, originalStart), new Span(0, originalStart))) : (Match)null; + } + + public static void AddDifference(int originalStart, int originalEnd, int nextOriginalEnd, + int modifiedStart, int modifiedEnd, int nextModifiedEnd, + IList<Difference> diffs, ref Match before) + { + Match after = (originalEnd != nextOriginalEnd) + ? (new Match(Span.FromBounds(originalEnd, nextOriginalEnd), Span.FromBounds(modifiedEnd, nextModifiedEnd))) + : (Match)null; + + diffs.Add(new Difference(Span.FromBounds(originalStart, originalEnd), Span.FromBounds(modifiedStart, modifiedEnd), + before, after)); + + before = after; + } + + /// <summary> + /// Get the original match sequence that was used to create this diff collection + /// </summary> + public IEnumerable<Tuple<int, int>> MatchSequence + { + get { return sequence; } + } + + /// <summary> + /// Get the left sequence that was used to create this diff collection + /// </summary> + public IList<T> LeftSequence + { + get { return originalLeft; } + } + + /// <summary> + /// Get the right sequence that was used to create this diff collection + /// </summary> + public IList<T> RightSequence + { + get { return originalRight; } + } + + /// <summary> + /// Get the differences as a list. + /// If you just want to enumerate over the differences, you can use + /// the DiffCollection directly, as it is IEnumerable. + /// </summary> + public IList<Difference> Differences + { + get { return this.diffs; } + } + + #region Private Helpers + + /// <summary> + /// Create a list of matches from a given ordered collection of + /// match pairs (e.g. a MatchSequence). + /// </summary> + /// <param name="matches">An ordered collection of matching pairs, like a MatchSequence</param> + /// <returns>An IList of the generated Matches</returns> + internal static IList<Match> MatchesFromPairs(IList<Tuple<int, int>> matches) + { + if (matches.Count == 0) + { + return new List<Match>(); + } + + IList<Match> mranges = new List<Match>(); + + Tuple<int, int> firstMatch = matches[0]; + int leftStart = firstMatch.Item1; + int leftEnd = leftStart + 1; + + int rightStart = firstMatch.Item2; + int rightEnd = rightStart + 1; + + for (int i = 1; i < matches.Count; i++) + { + Tuple<int, int> pair = matches[i]; + + if (pair.Item1 == leftEnd && + pair.Item2 == rightEnd) + { + leftEnd++; + rightEnd++; + } + else + { + mranges.Add(new Match(Span.FromBounds(leftStart, leftEnd), Span.FromBounds(rightStart, rightEnd))); + leftStart = pair.Item1; + leftEnd = leftStart + 1; + + rightStart = pair.Item2; + rightEnd = rightStart + 1; + } + } + + mranges.Add(new Match(Span.FromBounds(leftStart, leftEnd), Span.FromBounds(rightStart, rightEnd))); + + return mranges; + } + + #endregion + + #region IEnumerable<Difference> Members + + public IEnumerator<Difference> GetEnumerator() + { + return diffs.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } + + internal sealed class MatchEnumerator : IEnumerable<Tuple<int, int>> + { + IList<Difference> _differences; + int _leftCount; + + public MatchEnumerator(IList<Difference> differences, int leftCount) + { + _differences = differences; + _leftCount = leftCount; + } + + public IEnumerator<Tuple<int, int>> GetEnumerator() + { + int leftStart = 0; + int rightStart = 0; + if (_differences.Count != 0) + { + foreach (var difference in _differences) + { + Match m = difference.Before; + if (m != null) + { + for (int i = 0; i < m.Length; i++) + { + yield return new Tuple<int, int>(m.Left.Start + i, m.Right.Start + i); + } + } + + leftStart = difference.Left.End; + rightStart = difference.Right.End; + } + } + + for (int i = leftStart; (i < _leftCount); ++i) + { + yield return new Tuple<int, int>(i, i + rightStart - leftStart); + } + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Text/Util/TextDataUtil/ExtensionSelector.cs b/src/Text/Util/TextDataUtil/ExtensionSelector.cs new file mode 100644 index 0000000..2496711 --- /dev/null +++ b/src/Text/Util/TextDataUtil/ExtensionSelector.cs @@ -0,0 +1,71 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Helper class to perform ContentType best-match against a set of extensions. This could + /// become a public service. + /// </summary> + internal static class ExtensionSelector + { + /// <summary> + /// Given a list of extensions that provide content types, filter the list and return that + /// subset which matches the given content type + /// </summary> + public static List<Lazy<TProvider, TMetadataView>> SelectMatchingExtensions<TProvider, TMetadataView> + (IEnumerable<Lazy<TProvider, TMetadataView>> providerHandles, + IContentType dataContentType) + where TMetadataView : IContentTypeMetadata // content type is required + { + var result = new List<Lazy<TProvider, TMetadataView>>(); + foreach (var providerHandle in providerHandles) + { + if (ContentTypeMatch(dataContentType, providerHandle.Metadata.ContentTypes)) + { + result.Add(providerHandle); + } + } + return result; + } + + /// <summary> + /// Test whether an extension matches a content type. + /// </summary> + /// <param name="dataContentType">Content type (typically of a text buffer) against which to match an extension.</param> + /// <param name="extensionContentTypes">Content types from extension metadata.</param> + public static bool ContentTypeMatch(IContentType dataContentType, IEnumerable<string> extensionContentTypes) + { + foreach (string contentType in extensionContentTypes) + { + if (dataContentType.IsOfType(contentType)) + { + return true; + } + } + return false; + } + + /// <summary> + /// Test whether an extension matches one of a set of content types. + /// </summary> + /// <param name="dataContentTypes">Content types (typically of text buffers in a buffer graph) against which to match an extension.</param> + /// <param name="extensionContentTypes">Content types from extension metadata.</param> + public static bool ContentTypeMatch(IEnumerable<IContentType> dataContentTypes, IEnumerable<string> extensionContentTypes) + { + foreach (IContentType bufferContentType in dataContentTypes) + { + if (ContentTypeMatch(bufferContentType, extensionContentTypes)) + { + return true; + } + } + return false; + } + } +} diff --git a/src/Text/Util/TextDataUtil/FrugalList.cs b/src/Text/Util/TextDataUtil/FrugalList.cs new file mode 100644 index 0000000..fe554fe --- /dev/null +++ b/src/Text/Util/TextDataUtil/FrugalList.cs @@ -0,0 +1,416 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +#undef STATS + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + +#if STATS + static class Stats + { + public static int[] sizes = new int[20]; + } +#endif + + /// <summary> + /// <para> + /// This implementation is intended for lists that are usually empty or have a single element. + /// The element type may be a struct or a class, but lists of structs are by far the most common + /// in the editor. We store the head of the list in a local field and allocate an array for the tail of the + /// list only if it the list has length greater than one. Thus singleton and empty lists require only + /// a single object (not counting elements of the list), as compared to the BCL version of list, + /// which allocates two objects for a list with a single member. + /// </para> + /// <para> + /// Do not use this implementation for lists that you know will have length greater than two; the + /// platform list implementation will be more space efficient. + /// </para> + /// </summary> + /// <typeparam name="T">The type of the list element.</typeparam> + public class FrugalList<T> : IList<T>, IReadOnlyList<T> + { + const int InitialTailSize = 2; // initial size of array list + + static List<T> UnitaryTail = new List<T>(0); // marker that the FrugalList has length one. + + T head; // first element of list + List<T> tail; // the balance + +#if STATS + ~FrugalList() + { + Stats.sizes[Math.Min(19, this.count)]++; + } +#endif + #region Construction + // there is no constructor that takes a capacity because we are using + // the tail field to tell us something about the current count of elements + // in the list. If you are tempted to provide a capacity, and it's less than two, + // go ahead and use this list without a capacity. If it's greater than two, you should + // be using a regular list, which will be more frugal. + + public FrugalList() + { + } + + public FrugalList(IList<T> elements) + { + if (elements == null) + { + throw new ArgumentNullException("elements"); + } + switch (elements.Count) + { + case 0: + break; + case 1: + this.head = elements[0]; + this.tail = UnitaryTail; + break; + default: + this.head = elements[0]; + this.tail = new List<T>(InitialTailSize); + for (int i = 1; i < elements.Count; ++i) + { + this.tail.Add(elements[i]); + } + break; + } + } + #endregion + + public int Count + { + get + { + if (this.tail == null) + { + return 0; + } + else + { + return 1 + this.tail.Count; + } + } + } + + public void AddRange(IList<T> list) + { + if (list == null) + { + throw new ArgumentNullException("list"); + } + + for (int i = 0; i < list.Count; ++i) + { + Add(list[i]); + } + } + + public ReadOnlyCollection<T> AsReadOnly() + { + return new ReadOnlyCollection<T>(this); + } + + public int RemoveAll(Predicate<T> match) + { + if (match == null) + { + throw new ArgumentNullException("match"); + } + int removed = 0; + for (int i = Count - 1; i >= 0; --i) + { + if (match(this[i])) + { + removed++; + RemoveAt(i); + } + } + return removed; + } + + public void Add(T item) + { + if (Count == 0) + { + this.head = item; + this.tail = UnitaryTail; + } + else + { + Debug.Assert(this.tail != null); + if (this.tail == UnitaryTail) + { + this.tail = new List<T>(InitialTailSize); + } + this.tail.Add(item); + } + } + + public int IndexOf(T item) + { + if (Count > 0) + { + if (EqualityComparer<T>.Default.Equals(this.head, item)) + { + return 0; + } + else + { + int indexOf = this.tail.IndexOf(item); + return indexOf >= 0 ? indexOf + 1 : -1; + } + } + else + { + return -1; + } + } + + public void Insert(int index, T item) + { + if (index < 0 || index > this.Count) + { + throw new ArgumentOutOfRangeException("index"); + } + if (index == 0) + { + switch (Count) + { + case 0: + this.tail = UnitaryTail; + break; + case 1: + this.tail = new List<T>(InitialTailSize); + this.tail.Add(this.head); + break; + default: + this.tail.Insert(0, this.head); + break; + } + this.head = item; + } + else + { + Debug.Assert(Count > 0); + if (this.tail == UnitaryTail) + { + this.tail = new List<T>(InitialTailSize); + } + this.tail.Insert(index - 1, item); + } + } + + public void RemoveAt(int index) + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index"); + } + + int count = Count; + if (index == 0) + { + if (count == 1) + { + this.head = default(T); + this.tail = null; + } + else + { + this.head = this.tail[0]; + if (count == 2) + { + this.tail = UnitaryTail; + } + else + { + this.tail.RemoveAt(0); + } + } + } + else if (count == 2) + { + Debug.Assert(index == 1); + this.tail = UnitaryTail; + } + else + { + this.tail.RemoveAt(index - 1); + } + } + + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index"); + } + + if (index == 0) + { + return this.head; + } + else + { + return this.tail[index - 1]; + } + } + set + { + if (index < 0 || index >= Count) + { + throw new ArgumentOutOfRangeException("index"); + } + + if (index == 0) + { + this.head = value; + } + else + { + this.tail[index - 1] = value; + } + } + } + + public void Clear() + { + this.head = default(T); + this.tail = null; + } + + public bool Contains(T item) + { + int count = Count; + if (count > 0) + { + if (EqualityComparer<T>.Default.Equals(this.head, item)) + { + return true; + } + if (count > 1) + { + return this.tail.Contains(item); + } + } + return false; + } + + public void CopyTo(T[] array, int arrayIndex) + { + if (array == null) + { + throw new ArgumentNullException("array"); + } + int count = Count; + if (count > 0) + { + // let array indexing do the index checks + array[arrayIndex++] = this.head; + if (count > 1) + { + this.tail.CopyTo(array, arrayIndex); + } + } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool Remove(T item) + { + if (Count > 0) + { + if (EqualityComparer<T>.Default.Equals(this.head, item)) + { + RemoveAt(0); + return true; + } + else + { + return this.tail.Remove(item); + } + } + return false; + } + + IEnumerator<T> IEnumerable<T>.GetEnumerator() + { + return new FrugalEnumerator(this); + } + + public FrugalEnumerator GetEnumerator() + { + return new FrugalEnumerator(this); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return new FrugalEnumerator(this); + } + + public struct FrugalEnumerator : IEnumerator, IEnumerator<T> + { + FrugalList<T> list; + int position; + + public FrugalEnumerator(FrugalList<T> list) + { + this.list = list; + this.position = -1; + } + + public T Current + { + get + { + // if position is -1, then the behavior of Current is unspecified. + // for us it will throw an indexing exception. + return this.list[this.position]; + } + } + + object IEnumerator.Current + { + get { return this.list[this.position]; } + } + + public bool MoveNext() + { + if (this.position < this.list.Count - 1) + { + this.position++; + return true; + } + else + { + return false; + } + } + + public void Reset() + { + this.position = -1; + } + + public void Dispose() + { + } + } + } + +} diff --git a/src/Text/Util/TextDataUtil/GuardedOperations.cs b/src/Text/Util/TextDataUtil/GuardedOperations.cs new file mode 100644 index 0000000..a7412b2 --- /dev/null +++ b/src/Text/Util/TextDataUtil/GuardedOperations.cs @@ -0,0 +1,675 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using System.ComponentModel.Composition; + using System.Diagnostics; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.VisualStudio.Threading; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Operations that guard calls to suspicious code and log errors to registered extension error handlers. + /// </summary> + [Export] + [Export(typeof(IGuardedOperations))] + [PartCreationPolicy(CreationPolicy.Shared)] + internal sealed class GuardedOperations : IGuardedOperations + { + [ImportMany] + private List<Lazy<IExtensionErrorHandler>> _errorHandlerExports = null; + + [ImportMany] + private List<Lazy<IExtensionPerformanceTracker>> _perTrackerExports = null; + + [Import] + private JoinableTaskContext _joinableTaskContext; + + [Import(AllowDefault = true)] + internal INonJoinableTaskTrackerInternal NonJoinableTaskTracker; // Optional in scenarios other than in VS process. + + private FrugalList<IExtensionErrorHandler> _errorHandlers; + private FrugalList<IExtensionPerformanceTracker> _perfTrackers; + + public GuardedOperations() + { + } + + /// <summary> + /// For unit testing. + /// </summary> + internal GuardedOperations(JoinableTaskContext jtContext) + { + _joinableTaskContext = jtContext; + } + + /// <summary> + /// For unit testing. + /// </summary> + public GuardedOperations(params IExtensionErrorHandler[] extensionErrorHandler) + { + _errorHandlers = new FrugalList<IExtensionErrorHandler>(extensionErrorHandler); + _perfTrackers = new FrugalList<IExtensionPerformanceTracker>(); + } + + internal static bool ReThrowIfNoHandlers { get; set; } // For unit testing. + + private FrugalList<IExtensionErrorHandler> ErrorHandlers + { + get + { + if (_errorHandlers == null) + { + _errorHandlers = new FrugalList<IExtensionErrorHandler>(); + if (_errorHandlerExports != null) // can be null during unit testing + { + foreach (var export in _errorHandlerExports) + { + try + { + var handler = export.Value; + if (handler != null) + { + _errorHandlers.Add(handler); + } + } + catch (Exception) + { + Debug.Fail("Exception instantiating error handler!"); + } + } + } + } + return _errorHandlers; + } + } + + private FrugalList<IExtensionPerformanceTracker> PerfTrackers + { + get + { + if (_perfTrackers == null) + { + _perfTrackers = new FrugalList<IExtensionPerformanceTracker>(); + if (_perTrackerExports != null) // can be null during unit testing + { + foreach (var export in _perTrackerExports) + { + try + { + var perfTracker = export.Value; + if (perfTracker != null) + { + _perfTrackers.Add(perfTracker); + } + } + catch (Exception) + { + Debug.Fail("Exception instantiating perf tracker"); + } + } + } + } + return _perfTrackers; + } + } + + public TExtensionInstance InvokeBestMatchingFactory<TExtensionFactory, TExtensionInstance, TMetadataView> + (IList<Lazy<TExtensionFactory, TMetadataView>> providerHandles, + IContentType dataContentType, + Func<TExtensionFactory, TExtensionInstance> getter, + IContentTypeRegistryService contentTypeRegistryService, + object errorSource) + where TMetadataView : IContentTypeMetadata + where TExtensionFactory : class + { + var factory = InvokeBestMatchingFactory(providerHandles, dataContentType, contentTypeRegistryService, errorSource); + + if (factory == null) + { + return default(TExtensionInstance); + } + + TExtensionInstance extensionInstance = default(TExtensionInstance); + this.CallExtensionPoint(errorSource, () => extensionInstance = getter(factory)); + return extensionInstance; + } + + public TExtension InvokeBestMatchingFactory<TExtension, TMetadataView> + (IList<Lazy<TExtension, TMetadataView>> providerHandles, + IContentType dataContentType, + IContentTypeRegistryService contentTypeRegistryService, + object errorSource) + where TMetadataView : IContentTypeMetadata + { + var candidates = new List<Lazy<TExtension, TMetadataView>>(); + foreach (var providerHandle in providerHandles) + { + foreach (string contentTypeName in providerHandle.Metadata.ContentTypes) + { + if (string.Compare(dataContentType.TypeName, contentTypeName, StringComparison.OrdinalIgnoreCase) == 0) + { + // we have an exact match--no need to look further if this one is happy + TExtension factory = InstantiateExtension(errorSource, providerHandle); + if (factory != null) + { + return factory; + } + } + else if (dataContentType.IsOfType(contentTypeName)) + { + candidates.Add(providerHandle); + break; + } + } + } + + SortCandidates(candidates, dataContentType, contentTypeRegistryService); + + for (int c = 0; c < candidates.Count; ++c) + { + TExtension factory = InstantiateExtension(errorSource, candidates[c]); + if (factory != null) + { + return factory; + } + } + + // no suitable provider found + return default(TExtension); + } + + public List<TExtensionInstance> InvokeMatchingFactories<TExtensionInstance, TExtensionFactory, TMetadataView> + (IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, + Func<TExtensionFactory, TExtensionInstance> getter, + IContentType dataContentType, + object errorSource) + where TMetadataView : IContentTypeMetadata // content type is required + where TExtensionFactory : class + where TExtensionInstance : class + { + var result = new List<TExtensionInstance>(); + foreach (var lazyFactory in lazyFactories) + { + if (ExtensionSelector.ContentTypeMatch(dataContentType, lazyFactory.Metadata.ContentTypes)) + { + try + { + TExtensionFactory factory = lazyFactory.Value; + if (factory != null) + { + TExtensionInstance instance = getter(factory); + if (instance != null) + { + result.Add(instance); + } + } + } + catch (Exception e) + { + HandleException(errorSource, e); + } + } + } + return result; + } + + // The algorithm here is that assets can have a Name attribute and one or more Replaces attribute. + // Assets without names are treated normally (they are always considered eligible). + // Named assets are considered ineligible if: + // There is a "better" asset with the same name (better means a more specific content type). + // There is another assert with a Replaces attribute that matches the name of the asset. + public IEnumerable<Lazy<TExtensionFactory, TMetadataView>> FindEligibleFactories<TExtensionFactory, TMetadataView> + (IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, + IContentType dataContentType, + IContentTypeRegistryService contentTypeRegistryService) + where TMetadataView : INamedContentTypeMetadata // content type is required + where TExtensionFactory : class + { + Dictionary<string, List<Lazy<TExtensionFactory, TMetadataView>>> namedFactories = null; + HashSet<string> replaced = null; + foreach (var lazyFactory in lazyFactories) + { + if (ExtensionSelector.ContentTypeMatch(dataContentType, lazyFactory.Metadata.ContentTypes)) + { + if (string.IsNullOrEmpty(lazyFactory.Metadata.Name)) + { + yield return lazyFactory; + } + else + { + if (namedFactories == null) + { + namedFactories = new Dictionary<string, List<Lazy<TExtensionFactory, TMetadataView>>>(StringComparer.OrdinalIgnoreCase); + } + + List<Lazy<TExtensionFactory, TMetadataView>> factories; + if (!namedFactories.TryGetValue(lazyFactory.Metadata.Name, out factories)) + { + factories = new List<Lazy<TExtensionFactory, TMetadataView>>(); + namedFactories.Add(lazyFactory.Metadata.Name, factories); + } + + factories.Add(lazyFactory); + + if (lazyFactory.Metadata.Replaces != null) + { + if (replaced == null) + { + replaced = new HashSet<string>(StringComparer.OrdinalIgnoreCase); + } + + foreach (var s in lazyFactory.Metadata.Replaces) + { + replaced.Add(s); + } + } + } + } + } + + if (namedFactories != null) + { + foreach (var candidates in namedFactories.Values) + { + var candidate = candidates[0]; + if ((replaced == null) || !replaced.Contains(candidate.Metadata.Name)) + { + SortCandidates(candidates, dataContentType, contentTypeRegistryService); + yield return candidates[0]; + } + } + } + } + + + /// <summary> + /// Given a list of factory extensions that provide content types, filter the list, instantiate that + /// subset which matches the given content type, and invoke the factory method. Return the non-null results. + /// </summary> + public List<TExtensionInstance> InvokeEligibleFactories<TExtensionInstance, TExtensionFactory, TMetadataView> + (IEnumerable<Lazy<TExtensionFactory, TMetadataView>> lazyFactories, + Func<TExtensionFactory, TExtensionInstance> getter, + IContentType dataContentType, + IContentTypeRegistryService contentTypeRegistryService, + object errorSource) + where TMetadataView : INamedContentTypeMetadata // content type is required + where TExtensionFactory : class + where TExtensionInstance : class + { + var result = new List<TExtensionInstance>(); + foreach (var lazyFactory in FindEligibleFactories(lazyFactories, dataContentType, contentTypeRegistryService)) + { + try + { + TExtensionFactory factory = lazyFactory.Value; + if (factory != null) + { + TExtensionInstance instance = getter(factory); + if (instance != null) + { + result.Add(instance); + } + } + } + catch (Exception e) + { + HandleException(errorSource, e); + } + } + + return result; + } + + public TExtension InstantiateExtension<TExtension>(object errorSource, Lazy<TExtension> provider) + { + try + { + return provider.Value; + } + catch (Exception e) + { + HandleException(errorSource, e); + return default(TExtension); + } + } + + public TExtension InstantiateExtension<TExtension, TMetadata>(object errorSource, Lazy<TExtension, TMetadata> provider) + { + try + { + return provider.Value; + } + catch (Exception e) + { + HandleException(errorSource, e); + return default(TExtension); + } + } + + public TExtensionInstance InstantiateExtension<TExtension, TMetadata, TExtensionInstance>( + object errorSource, Lazy<TExtension, TMetadata> provider, Func<TExtension, TExtensionInstance> getter) + { + try + { + return getter(provider.Value); + } + catch (Exception e) + { + HandleException(errorSource, e); + return default(TExtensionInstance); + } + } + + public void CallExtensionPoint(object errorSource, Action call) + { + try + { + BeforeCallingEventHandler(call); + call(); + } + catch (Exception e) + { + HandleException(errorSource, e); + } + finally + { + AfterCallingEventHandler(call); + } + } + + public T CallExtensionPoint<T>(object errorSource, Func<T> call, T valueOnThrow) + { + try + { + BeforeCallingEventHandler(call); + return call(); + } + catch (Exception e) + { + HandleException(errorSource, e); + + return valueOnThrow; + } + finally + { + AfterCallingEventHandler(call); + } + } + + public void CallExtensionPoint(Action call) + { + this.CallExtensionPoint(errorSource: null, call: call); + } + + public T CallExtensionPoint<T>(Func<T> call, T valueOnThrow) + { + return this.CallExtensionPoint(errorSource: null, call: call, valueOnThrow: valueOnThrow); + } + + public async Task CallExtensionPointAsync(object errorSource, Func<Task> asyncAction) + { + try + { + await asyncAction(); + } + catch (Exception e) + { + HandleException(errorSource, e); + } + } + + public async Task CallExtensionPointAsync(Func<Task> asyncAction) + { + await CallExtensionPointAsync(errorSource: null, asyncAction: asyncAction); + } + + public async Task<T> CallExtensionPointAsync<T>(object errorSource, Func<Task<T>> asyncCall, T valueOnThrow) + { + try + { + return await asyncCall(); + } + catch (Exception e) + { + HandleException(errorSource, e); + return valueOnThrow; + } + } + + public async Task<T> CallExtensionPointAsync<T>(Func<Task<T>> asyncCall, T valueOnThrow) + { + return await CallExtensionPointAsync<T>(errorSource: null, asyncCall: asyncCall, valueOnThrow: valueOnThrow); + } + + public void RaiseEvent(object sender, EventHandler eventHandlers) + { + if (eventHandlers == null) + { + return; + } + + var handlers = eventHandlers.GetInvocationList(); + + foreach (EventHandler handler in handlers) + { + try + { + BeforeCallingEventHandler(handler); + handler(sender, EventArgs.Empty); + } + catch (Exception e) + { + HandleException(sender, e); + } + finally + { + AfterCallingEventHandler(handler); + } + } + } + + public void RaiseEvent<TArgs>(object sender, EventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs + { + if (eventHandlers == null) + { + return; + } + var handlers = eventHandlers.GetInvocationList(); + + foreach (EventHandler<TArgs> handler in handlers) + { + try + { + BeforeCallingEventHandler(handler); + handler(sender, args); + } + catch (Exception e) + { + HandleException(sender, e); + } + finally + { + AfterCallingEventHandler(handler); + } + } + } + + private void AfterCallingEventHandler(Delegate handler) + { + if (PerfTrackers.Count == 0) + { + return; + } + + foreach (var perfTracker in PerfTrackers) + { + try + { + perfTracker.AfterCallingEventHandler(handler); + } + catch (Exception e) + { + HandleException(perfTracker, e); + } + } + } + + private void BeforeCallingEventHandler(Delegate handler) + { + if (PerfTrackers.Count == 0) + { + return; + } + + foreach (var perfTracker in PerfTrackers) + { + try + { + perfTracker.BeforeCallingEventHandler(handler); + } + catch (Exception e) + { + HandleException(perfTracker, e); + } + } + } + + public void HandleException(object errorSource, Exception e) + { + bool handled = false; + foreach (var errorHandler in ErrorHandlers) + { + try + { + errorHandler.HandleError(errorSource, e); + handled = true; + } + catch (Exception doubleFaultException) + { + // TODO: What is the right behavior here? + Debug.Fail(doubleFaultException.ToString()); + } + } + if (!handled) + { + // TODO: What is the right behavior here? + Debug.Fail(e.ToString()); + + if (GuardedOperations.ReThrowIfNoHandlers) + throw new Exception("Unhandled exception.", e); + } + } + + private static void SortCandidates<TExtension, TMetadataView>(List<Lazy<TExtension, TMetadataView>> candidates, IContentType dataContentType, IContentTypeRegistryService contentTypeRegistryService) + where TMetadataView : IContentTypeMetadata + { + if (candidates.Count > 1) + { + var contentTypes = new List<IContentType>(); + foreach (var c in candidates) + { + foreach (string contentTypeName in c.Metadata.ContentTypes) + { + if (dataContentType.IsOfType(contentTypeName)) + { + var type = contentTypeRegistryService.GetContentType(contentTypeName); + if (!contentTypes.Contains(type)) + { + contentTypes.Add(type); + } + } + } + } + + contentTypes.Sort(CompareContentTypes); + candidates.Sort((left, right) => + { + int leftIndex = BestContentTypeScore(left.Metadata.ContentTypes, contentTypes); + int rightIndex = BestContentTypeScore(right.Metadata.ContentTypes, contentTypes); + + return leftIndex - rightIndex; // Sort these in ascending order. + }); + } + } + + private static int BestContentTypeScore(IEnumerable<string> contentTypes, List<IContentType> sortedContentTypes) + { + return contentTypes.Min(s => ContentTypeScore(s, sortedContentTypes)); + } + + private static int ContentTypeScore(string contentTypeName, List<IContentType> sortedContentTypes) + { + for (int i = 0; (i < sortedContentTypes.Count); ++i) + { + if (string.Compare(sortedContentTypes[i].TypeName, contentTypeName, StringComparison.OrdinalIgnoreCase) == 0) + { + return i; + } + } + + return sortedContentTypes.Count; + } + + private static int CompareContentTypes(IContentType left, IContentType right) + { + if (left == right) + { + return 0; + } + else + { + if (left.IsOfType(right.TypeName)) + { + return -1; + } + else if (right.IsOfType(left.TypeName)) + { + return +1; + } + else + { + // the content types are unrelated, use alpha order of their names + return string.Compare(left.TypeName, right.TypeName, StringComparison.OrdinalIgnoreCase); + } + } + } + + public Task RaiseEventOnBackgroundAsync<TArgs>(object sender, AsyncEventHandler<TArgs> eventHandlers, TArgs args) where TArgs : EventArgs + { + return _joinableTaskContext.Factory.RunAsync(async () => + { + await TaskScheduler.Default; + + if (eventHandlers == null) + { + return; + } + + var handlers = eventHandlers.GetInvocationList(); + + foreach (AsyncEventHandler<TArgs> handler in handlers) + { + try + { + BeforeCallingEventHandler(handler); + await handler(sender, args); + } + catch (Exception e) + { + HandleException(sender, e); + } + finally + { + AfterCallingEventHandler(handler); + } + } + }).Task; + } + } +} diff --git a/src/Text/Util/TextDataUtil/IEncodingDetectorMetadata.cs b/src/Text/Util/TextDataUtil/IEncodingDetectorMetadata.cs new file mode 100644 index 0000000..38dfe0c --- /dev/null +++ b/src/Text/Util/TextDataUtil/IEncodingDetectorMetadata.cs @@ -0,0 +1,15 @@ +// +// 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.Utilities +{ + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// MEF metadata definition for <see cref="IEncodingDetector"/>. + /// </summary> + public interface IEncodingDetectorMetadata : IOrderable, IContentTypeMetadata + { + } +} diff --git a/src/Text/Util/TextDataUtil/IOrderableContentTypeMetadata.cs b/src/Text/Util/TextDataUtil/IOrderableContentTypeMetadata.cs new file mode 100644 index 0000000..76f7e97 --- /dev/null +++ b/src/Text/Util/TextDataUtil/IOrderableContentTypeMetadata.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Metadata which includes Ordering and Content Types + /// </summary> + public interface IOrderableContentTypeMetadata : IContentTypeMetadata, IOrderable + { + } +} diff --git a/src/Text/Util/TextDataUtil/ListUtilities.cs b/src/Text/Util/TextDataUtil/ListUtilities.cs new file mode 100644 index 0000000..a61d5cb --- /dev/null +++ b/src/Text/Util/TextDataUtil/ListUtilities.cs @@ -0,0 +1,64 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// <summary> + /// Handy list-oriented utilities. + /// </summary> + internal static class ListUtilities + { + /// <summary> + /// Do a binary search in <paramref name="list"/> for an element that matches <paramref name="target"/> + /// </summary> + /// <param name="list">List to search.</param> + /// <param name="target">Object of the search.</param> + /// <param name="compare">Comparison function between an element and target (returns < 0 if e comes before t, 0 if e matches, > 0 if e comes after). + /// <param name="index">Index of the matching element (or, if there is no exact match, index of the element that follows it).</param> + /// <returns>true if an exact match was found.</returns> + /// <remarks>Yes, I know there is List.BinarySearch but that doesn't do exactly what I need most of the time.</remarks> + public static bool BinarySearch<E>(IList<E> list, Func<E, int> compare, out int index) + { + int lo = 0; + int hi = list.Count; + + while (lo < hi) + { + index = (lo + hi) / 2; + + int cmp = compare(list[index]); + if (cmp < 0) + { + lo = index + 1; + } + else if (cmp == 0) + { + return true; + } + else + { + hi = index; + } + } + + index = lo; + return false; + } + + public static T? FirstOrNullable<T>(this IEnumerable<T> source) + where T : struct + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + return source.Cast<T?>().FirstOrDefault(); + } + } + } diff --git a/src/Text/Util/TextDataUtil/MappingHelper.cs b/src/Text/Util/TextDataUtil/MappingHelper.cs new file mode 100644 index 0000000..add43f2 --- /dev/null +++ b/src/Text/Util/TextDataUtil/MappingHelper.cs @@ -0,0 +1,275 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Projection; + + internal static class MappingHelper + { + //These two methods are nearly duplicates of one another but delegates can be expensive and this is inner loop code. + internal static ITextSnapshot FindCorrespondingSnapshot(ITextSnapshot sourceSnapshot, ITextBuffer targetBuffer) + { + if (sourceSnapshot.TextBuffer == targetBuffer) + { + // simple case: single buffer + return sourceSnapshot; + } + else + { + IProjectionSnapshot2 projSnap = sourceSnapshot as IProjectionSnapshot2; + if (projSnap != null) + { + return projSnap.GetMatchingSnapshotInClosure(targetBuffer); + } + else + { + return null; + } + } + } + + internal static ITextSnapshot FindCorrespondingSnapshot(ITextSnapshot sourceSnapshot, Predicate<ITextBuffer> match) + { + if (match(sourceSnapshot.TextBuffer)) + { + // simple case: single buffer + return sourceSnapshot; + } + else + { + IProjectionSnapshot2 projSnap = sourceSnapshot as IProjectionSnapshot2; + if (projSnap != null) + { + return projSnap.GetMatchingSnapshotInClosure(match); + } + else + { + return null; + } + } + } + + internal static NormalizedSnapshotSpanCollection MapDownToBufferNoTrack(SnapshotSpan sourceSpan, ITextBuffer targetBuffer, bool mapByContentType = false) + { + FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>(); + + MapDownToFirstMatchNoTrack(sourceSpan, (ITextBuffer b) => { return b == targetBuffer; }, mappedSpans, mapByContentType); + + return new NormalizedSnapshotSpanCollection(mappedSpans); + } + + internal static void MapDownToBufferNoTrack(SnapshotSpan sourceSpan, ITextBuffer targetBuffer, IList<SnapshotSpan> mappedSpans, bool mapByContentType = false) + { + // Most of the time, the sourceSpan will map to the targetBuffer as a single span, rather than being split. + // Since this method is called a lot, we'll assume first that we'll get a single span and don't need to + // allocate a stack to keep track of unmapped spans. If that fails we'll fall back on the more expensive approach. + // Scroll around for a while and this saves a bunch of allocations. + SnapshotSpan mappedSpan = sourceSpan; + while (true) + { + if (mappedSpan.Snapshot.TextBuffer == targetBuffer) + { + mappedSpans.Add(mappedSpan); + return; + } + else + { + IProjectionSnapshot mappedSpanProjectionSnapshot = mappedSpan.Snapshot as IProjectionSnapshot; + if (mappedSpanProjectionSnapshot != null && + (!mapByContentType || mappedSpanProjectionSnapshot.ContentType.IsOfType("projection"))) + { + var mappedDownSpans = mappedSpanProjectionSnapshot.MapToSourceSnapshots(mappedSpan); + if (mappedDownSpans.Count == 1) + { + mappedSpan = mappedDownSpans[0]; + continue; + } + else if (mappedDownSpans.Count == 0) + { + return; + } + else + { + // the projection mapping resulted in more than one span + FrugalList<SnapshotSpan> unmappedSpans = new FrugalList<SnapshotSpan>(mappedDownSpans); + SplitMapDownToBufferNoTrack(unmappedSpans, targetBuffer, mappedSpans, mapByContentType); + return; + } + } + else + { + // either it's a projection buffer we can't look through, or it's + // an ordinary buffer that didn't match + return; + } + } + } + } + + private static void SplitMapDownToBufferNoTrack(FrugalList<SnapshotSpan> unmappedSpans, ITextBuffer targetBuffer, IList<SnapshotSpan> mappedSpans, bool mapByContentType) + { + while (unmappedSpans.Count > 0) + { + SnapshotSpan span = unmappedSpans[unmappedSpans.Count - 1]; + unmappedSpans.RemoveAt(unmappedSpans.Count - 1); + + if (span.Snapshot.TextBuffer == targetBuffer) + { + mappedSpans.Add(span); + } + else + { + IProjectionSnapshot spanSnapshotAsProjection = span.Snapshot as IProjectionSnapshot; + if (spanSnapshotAsProjection != null && + (!mapByContentType || span.Snapshot.TextBuffer.ContentType.IsOfType("projection"))) + { + unmappedSpans.AddRange(spanSnapshotAsProjection.MapToSourceSnapshots(span)); + } + } + } + } + + internal static void MapDownToFirstMatchNoTrack(SnapshotSpan sourceSpan, Predicate<ITextBuffer> match, IList<SnapshotSpan> mappedSpans, bool mapByContentType = false) + { + // Most of the time, the sourceSpan will map to the targetBuffer as a single span, rather than being split. + // Since this method is called a lot, we'll assume first that we'll get a single span and don't need to + // allocate a stack to keep track of unmapped spans. If that fails we'll fall back on the more expensive approach. + // Scroll around for a while and this saves a bunch of allocations. + SnapshotSpan mappedSpan = sourceSpan; + while (true) + { + if (match(mappedSpan.Snapshot.TextBuffer)) + { + mappedSpans.Add(mappedSpan); + return; + } + else + { + IProjectionSnapshot mappedSpanProjectionSnapshot = mappedSpan.Snapshot as IProjectionSnapshot; + if (mappedSpanProjectionSnapshot != null && + (!mapByContentType || mappedSpanProjectionSnapshot.ContentType.IsOfType("projection"))) + { + var mappedDownSpans = mappedSpanProjectionSnapshot.MapToSourceSnapshots(mappedSpan); + if (mappedDownSpans.Count == 1) + { + mappedSpan = mappedDownSpans[0]; + continue; + } + else if (mappedDownSpans.Count == 0) + { + return; + } + else + { + // the projection mapping resulted in more than one span + FrugalList<SnapshotSpan> unmappedSpans = new FrugalList<SnapshotSpan>(mappedDownSpans); + SplitMapDownToFirstMatchNoTrack(unmappedSpans, match, mappedSpans, mapByContentType); + return; + } + } + else + { + // either it's a projection buffer we can't look through, or it's + // an ordinary buffer that didn't match + return; + } + } + } + } + + private static void SplitMapDownToFirstMatchNoTrack(FrugalList<SnapshotSpan> unmappedSpans, Predicate<ITextBuffer> match, IList<SnapshotSpan> mappedSpans, bool mapByContentType) + { + ITextSnapshot matchingSnapshot = null; + + while (unmappedSpans.Count > 0) + { + SnapshotSpan span = unmappedSpans[unmappedSpans.Count - 1]; + unmappedSpans.RemoveAt(unmappedSpans.Count - 1); + + if (span.Snapshot == matchingSnapshot) + { + mappedSpans.Add(span); + } + else if (match(span.Snapshot.TextBuffer)) + { + mappedSpans.Add(span); + matchingSnapshot = span.Snapshot; + } + else + { + IProjectionSnapshot spanSnapshotAsProjection = span.Snapshot as IProjectionSnapshot; + if (spanSnapshotAsProjection != null && + (!mapByContentType || span.Snapshot.TextBuffer.ContentType.IsOfType("projection"))) + { + unmappedSpans.AddRange(spanSnapshotAsProjection.MapToSourceSnapshots(span)); + } + } + } + } + + internal static SnapshotPoint? MapDownToBufferNoTrack(SnapshotPoint position, ITextBuffer targetBuffer, PositionAffinity affinity) + { + while (position.Snapshot.TextBuffer != targetBuffer) + { + IProjectionSnapshot projSnap = position.Snapshot as IProjectionSnapshot; + if ((projSnap == null) || (projSnap.SourceSnapshots.Count == 0)) + { + return null; + } + + position = projSnap.MapToSourceSnapshot(position, affinity); + } + return position; + } + + internal static SnapshotPoint? MapDownToFirstMatchNoTrack(SnapshotPoint position, Predicate<ITextBuffer> match, PositionAffinity affinity) + { + while (!match(position.Snapshot.TextBuffer)) + { + IProjectionSnapshot projSnap = position.Snapshot as IProjectionSnapshot; + if ((projSnap == null) || (projSnap.SourceSnapshots.Count == 0)) + { + return null; + } + + position = projSnap.MapToSourceSnapshot(position, affinity); + } + return position; + } + + internal static SnapshotPoint? MapDownToBufferNoTrack(SnapshotPoint position, ITextBuffer targetBuffer) + { + while (position.Snapshot.TextBuffer != targetBuffer) + { + IProjectionSnapshot projSnap = position.Snapshot as IProjectionSnapshot; + if ((projSnap == null) || (projSnap.SourceSnapshots.Count == 0)) + { + return null; + } + + position = projSnap.MapToSourceSnapshot(position); + } + + return position; + } + + internal static SnapshotPoint? MapDownToFirstMatchNoTrack(SnapshotPoint position, Predicate<ITextBuffer> match) + { + while (!match(position.Snapshot.TextBuffer)) + { + IProjectionSnapshot projSnap = position.Snapshot as IProjectionSnapshot; + if ((projSnap == null) || (projSnap.SourceSnapshots.Count == 0)) + { + return null; + } + + position = projSnap.MapToSourceSnapshot(position); + } + return position; + } + } +} diff --git a/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs b/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs new file mode 100644 index 0000000..3e6c110 --- /dev/null +++ b/src/Text/Util/TextDataUtil/MappingPointSnapshot.cs @@ -0,0 +1,169 @@ +// +// 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.Utilities +{ + using System; + using Microsoft.VisualStudio.Text.Projection; + + internal class MappingPointSnapshot : IMappingPoint + { + internal ITextSnapshot _root; + internal SnapshotPoint _anchor; + internal PointTrackingMode _trackingMode; + IBufferGraph _graph; + internal bool _unmappable = false; + + public static IMappingPoint Create(ITextSnapshot root, SnapshotPoint anchor, PointTrackingMode trackingMode, IBufferGraph graph) + { + return new MappingPointSnapshot(root, anchor, trackingMode, graph); + } + + private MappingPointSnapshot(ITextSnapshot root, SnapshotPoint anchor, PointTrackingMode trackingMode, IBufferGraph graph) + { + //Anchor and root are expected to be from concurrent snapshots + ITextSnapshot correspondingAnchorSnapshot = MappingHelper.FindCorrespondingSnapshot(root, anchor.Snapshot.TextBuffer); + + _root = root; + if (correspondingAnchorSnapshot != null) + _anchor = anchor.TranslateTo(correspondingAnchorSnapshot, trackingMode); + else + { + _anchor = anchor; + _unmappable = true; + } + _trackingMode = trackingMode; + _graph = graph; + } + + public SnapshotPoint? GetPoint(ITextBuffer targetBuffer, PositionAffinity affinity) + { + if (targetBuffer == null) + throw new ArgumentNullException("targetBuffer"); + if (_unmappable) + return null; + + //Try mapping down first. + SnapshotPoint? mappedPoint = MappingHelper.MapDownToBufferNoTrack(_anchor, targetBuffer, affinity); + + if (!mappedPoint.HasValue) + { + //Unable to map down ... try mapping up. + return this.MapUpToBufferNoTrack(targetBuffer, affinity); + } + + return mappedPoint; + } + + public SnapshotPoint? GetPoint(ITextSnapshot targetSnapshot, PositionAffinity affinity) + { + if (targetSnapshot == null) + throw new ArgumentNullException("targetSnapshot"); + if (_unmappable) + return null; + + SnapshotPoint? mappedPoint = this.GetPoint(targetSnapshot.TextBuffer, affinity); + if (mappedPoint.HasValue && (mappedPoint.Value.Snapshot != targetSnapshot)) + { + mappedPoint = mappedPoint.Value.TranslateTo(targetSnapshot, _trackingMode); + } + + return mappedPoint; + } + + public SnapshotPoint? GetPoint(Predicate<ITextBuffer> match, PositionAffinity affinity) + { + if (match == null) + throw new ArgumentNullException("match"); + if (_unmappable) + return null; + + //Try mapping down first. + SnapshotPoint? mappedPoint = MappingHelper.MapDownToFirstMatchNoTrack(_anchor, match, affinity); + + if (!mappedPoint.HasValue) + { + //Unable to map down ... try mapping up. + return this.MapUpToFirstMatchNoTrack(match, affinity); + } + + return mappedPoint; + } + + public SnapshotPoint? GetInsertionPoint(Predicate<ITextBuffer> match) + { + if (match == null) + throw new ArgumentNullException("match"); + if (_unmappable) + return null; + + return MappingHelper.MapDownToFirstMatchNoTrack(_anchor, match); + } + + public ITextBuffer AnchorBuffer + { + get { return _anchor.Snapshot.TextBuffer; } + } + + public IBufferGraph BufferGraph + { + get { return _graph; } + } + + private SnapshotPoint? MapUpToBufferNoTrack(ITextBuffer targetBuffer, PositionAffinity affinity) + { + ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, targetBuffer); + if (targetSnapshot != null) + { + //Map _anchor up to targetSnapshot (they should be concurrent snapshots) + return MapUpToSnapshotNoTrack(targetSnapshot, affinity); + } + + return null; + } + + private SnapshotPoint? MapUpToFirstMatchNoTrack(Predicate<ITextBuffer> match, PositionAffinity affinity) + { + ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, match); + if (targetSnapshot != null) + { + //Map _anchor up to targetSnapshot (they should be concurrent snapshots) + return MapUpToSnapshotNoTrack(targetSnapshot, affinity); + } + + return null; + } + + private SnapshotPoint? MapUpToSnapshotNoTrack(ITextSnapshot targetSnapshot, PositionAffinity affinity) + { + return MapUpToSnapshotNoTrack(targetSnapshot, _anchor, affinity); + } + + public static SnapshotPoint? MapUpToSnapshotNoTrack(ITextSnapshot targetSnapshot, SnapshotPoint anchor, PositionAffinity affinity) + { + if (anchor.Snapshot == targetSnapshot) + return anchor; + else + { + IProjectionSnapshot targetAsProjection = targetSnapshot as IProjectionSnapshot; + if (targetAsProjection != null) + { + var sourceSnapshots = targetAsProjection.SourceSnapshots; + for (int s = 0; s < sourceSnapshots.Count; ++s) + { + SnapshotPoint? downPoint = MapUpToSnapshotNoTrack(sourceSnapshots[s], anchor, affinity); + if (downPoint.HasValue) + { + SnapshotPoint? result = targetAsProjection.MapFromSourceSnapshot(downPoint.Value, affinity); + if (result.HasValue) + return result; + } + } + } + } + + return null; + } + } +} diff --git a/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs b/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs new file mode 100644 index 0000000..88714fd --- /dev/null +++ b/src/Text/Util/TextDataUtil/MappingSpanSnapshot.cs @@ -0,0 +1,209 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Projection; + + internal class MappingSpanSnapshot : IMappingSpan + { + private ITextSnapshot _root; + private SnapshotSpan _anchor; + private SpanTrackingMode _trackingMode; + private IBufferGraph _graph; + private bool _unmappable = false; + + public static IMappingSpan Create(ITextSnapshot root, SnapshotSpan anchor, SpanTrackingMode trackingMode, IBufferGraph graph) + { + return new MappingSpanSnapshot(root, anchor, trackingMode, graph); + } + + private MappingSpanSnapshot(ITextSnapshot root, SnapshotSpan anchor, SpanTrackingMode trackingMode, IBufferGraph graph) + { + //Anchor and root are expected to be from concurrent snapshots + ITextSnapshot correspondingAnchorSnapshot = MappingHelper.FindCorrespondingSnapshot(root, anchor.Snapshot.TextBuffer); + + _root = root; + if (correspondingAnchorSnapshot != null) + _anchor = anchor.TranslateTo(correspondingAnchorSnapshot, trackingMode); + else + { + _anchor = anchor; + _unmappable = true; + } + _trackingMode = trackingMode; + _graph = graph; + } + + public NormalizedSnapshotSpanCollection GetSpans(ITextBuffer targetBuffer) + { + if (targetBuffer == null) + throw new ArgumentNullException("targetBuffer"); + + if (_unmappable) + return NormalizedSnapshotSpanCollection.Empty; + + if (targetBuffer.Properties.ContainsProperty("IdentityMapping")) + { + // text buffer properties uses the hybrid dictionary, which requires TWO lookups to determine that + // a key is not present. Since this test usually fails, do it the hard way (the second lookup shows up + // in scrolling profiles). + ITextBuffer doppelganger = (ITextBuffer)targetBuffer.Properties["IdentityMapping"]; + if (doppelganger == _anchor.Snapshot.TextBuffer) + { + // We are mapping up from a doppelganger buffer; the coordinates will be the same. We + // just need to figure out the right snapshot. + ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, targetBuffer); + return new NormalizedSnapshotSpanCollection(new SnapshotSpan(targetSnapshot, _anchor.Span)); + } + } + + //Try mapping down first. + FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>(); + MappingHelper.MapDownToBufferNoTrack(_anchor, targetBuffer, mappedSpans); + if (mappedSpans.Count == 0) + { + //Unable to map down ... try mapping up. + this.MapUpToBufferNoTrack(targetBuffer, mappedSpans); + } + + return new NormalizedSnapshotSpanCollection(mappedSpans); + } + + public NormalizedSnapshotSpanCollection GetSpans(ITextSnapshot targetSnapshot) + { + if (targetSnapshot == null) + throw new ArgumentNullException("targetSnapshot"); + if (_unmappable) + return NormalizedSnapshotSpanCollection.Empty; + + NormalizedSnapshotSpanCollection results = this.GetSpans(targetSnapshot.TextBuffer); + + if ((results.Count > 0) && (results[0].Snapshot != targetSnapshot)) + { + FrugalList<SnapshotSpan> translatedSpans = new FrugalList<SnapshotSpan>(); + foreach (SnapshotSpan s in results) + { + translatedSpans.Add(s.TranslateTo(targetSnapshot, _trackingMode)); + } + + results = new NormalizedSnapshotSpanCollection(translatedSpans); + } + + return results; + } + + public NormalizedSnapshotSpanCollection GetSpans(Predicate<ITextBuffer> match) + { + if (_unmappable) + return NormalizedSnapshotSpanCollection.Empty; + + //Try mapping down first. + FrugalList<SnapshotSpan> mappedSpans = new FrugalList<SnapshotSpan>(); + MappingHelper.MapDownToFirstMatchNoTrack(_anchor, match, mappedSpans); + if (mappedSpans.Count == 0) + { + //Unable to map down ... try mapping up. + this.MapUpToBufferNoTrack(match, mappedSpans); + } + + return new NormalizedSnapshotSpanCollection(mappedSpans); + } + + public IMappingPoint Start + { + get + { + return MappingPointSnapshot.Create(_root, _anchor.Start, + (_trackingMode == SpanTrackingMode.EdgeInclusive || + _trackingMode == SpanTrackingMode.EdgeNegative) + ? PointTrackingMode.Negative + : PointTrackingMode.Positive, + _graph); + } + } + + public IMappingPoint End + { + get + { + return MappingPointSnapshot.Create(_root, _anchor.End, + (_trackingMode == SpanTrackingMode.EdgeExclusive || + _trackingMode == SpanTrackingMode.EdgeNegative) + ? PointTrackingMode.Negative + : PointTrackingMode.Positive, + _graph); + } + } + + public ITextBuffer AnchorBuffer + { + get { return _anchor.Snapshot.TextBuffer; } + } + + public IBufferGraph BufferGraph + { + get { return _graph; } + } + + private void MapUpToBufferNoTrack(ITextBuffer targetBuffer, FrugalList<SnapshotSpan> mappedSpans) + { + ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, targetBuffer); + if (targetSnapshot != null) + { + //Map _anchor up to targetSnapshot (they should be concurrent snapshots) + MapUpToSnapshotNoTrack(targetSnapshot, mappedSpans); + } + } + + private void MapUpToBufferNoTrack(Predicate<ITextBuffer> match, FrugalList<SnapshotSpan> mappedSpans) + { + ITextSnapshot targetSnapshot = MappingHelper.FindCorrespondingSnapshot(_root, match); + if (targetSnapshot != null) + { + //Map _anchor up to targetSnapshot (they should be concurrent snapshots) + MapUpToSnapshotNoTrack(targetSnapshot, mappedSpans); + } + } + + private void MapUpToSnapshotNoTrack(ITextSnapshot targetSnapshot, FrugalList<SnapshotSpan> mappedSpans) + { + MappingSpanSnapshot.MapUpToSnapshotNoTrack(targetSnapshot, _anchor, mappedSpans); + } + + public static void MapUpToSnapshotNoTrack(ITextSnapshot targetSnapshot, SnapshotSpan anchor, IList<SnapshotSpan> mappedSpans) + { + if (anchor.Snapshot == targetSnapshot) + mappedSpans.Add(anchor); + else + { + IProjectionSnapshot targetAsProjection = targetSnapshot as IProjectionSnapshot; + if (targetAsProjection != null) + { + var sourceSnapshots = targetAsProjection.SourceSnapshots; + for (int s = 0; s < sourceSnapshots.Count; ++s) + { + FrugalList<SnapshotSpan> downSpans = new FrugalList<SnapshotSpan>(); + MapUpToSnapshotNoTrack(sourceSnapshots[s], anchor, downSpans); + for (int ds = 0; ds < downSpans.Count; ++ds) + { + var upSpans = targetAsProjection.MapFromSourceSnapshot(downSpans[ds]); + for (int us = 0; us < upSpans.Count; ++us) + { + mappedSpans.Add(new SnapshotSpan(targetSnapshot, upSpans[us])); + } + } + } + } + } + } + + public override string ToString() + { + return String.Format("MappingSpanSnapshot anchored at {0}", _anchor); + } + } +} diff --git a/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs b/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs new file mode 100644 index 0000000..dc61741 --- /dev/null +++ b/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.Enumerator.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + internal partial class ArrayBuilder<T> + { + /// <summary> + /// struct enumerator used in foreach. + /// </summary> + internal struct Enumerator : IEnumerator<T> + { + private readonly ArrayBuilder<T> _builder; + private int _index; + + public Enumerator(ArrayBuilder<T> builder) + { + _builder = builder; + _index = -1; + } + + public T Current + { + get + { + return _builder[_index]; + } + } + + public bool MoveNext() + { + _index++; + return _index < _builder.Count; + } + + public void Dispose() + { + } + + object System.Collections.IEnumerator.Current + { + get + { + return this.Current; + } + } + + public void Reset() + { + _index = -1; + } + } + } +} diff --git a/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs b/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs new file mode 100644 index 0000000..7e205b3 --- /dev/null +++ b/src/Text/Util/TextDataUtil/PooledObjects/ArrayBuilder.cs @@ -0,0 +1,527 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + [DebuggerDisplay("Count = {Count,nq}")] + [DebuggerTypeProxy(typeof(ArrayBuilder<>.DebuggerProxy))] + internal sealed partial class ArrayBuilder<T> : IReadOnlyCollection<T>, IReadOnlyList<T> + { + #region DebuggerProxy + + private sealed class DebuggerProxy + { + private readonly ArrayBuilder<T> _builder; + + public DebuggerProxy(ArrayBuilder<T> builder) + { + _builder = builder; + } + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public T[] A + { + get + { + var result = new T[_builder.Count]; + for (int i = 0; i < result.Length; i++) + { + result[i] = _builder[i]; + } + + return result; + } + } + } + + #endregion + + private readonly ImmutableArray<T>.Builder _builder; + + private readonly ObjectPool<ArrayBuilder<T>> _pool; + + public ArrayBuilder(int size) + { + _builder = ImmutableArray.CreateBuilder<T>(size); + } + + public ArrayBuilder() : + this(8) + { } + + private ArrayBuilder(ObjectPool<ArrayBuilder<T>> pool) : + this() + { + _pool = pool; + } + + /// <summary> + /// Realizes the array. + /// </summary> + public ImmutableArray<T> ToImmutable() + { + return _builder.ToImmutable(); + } + + public int Count + { + get + { + return _builder.Count; + } + set + { + _builder.Count = value; + } + } + + public T this[int index] + { + get + { + return _builder[index]; + } + + set + { + _builder[index] = value; + } + } + + /// <summary> + /// Write <paramref name="value"/> to slot <paramref name="index"/>. + /// Fills in unallocated slots preceding the <paramref name="index"/>, if any. + /// </summary> + public void SetItem(int index, T value) + { + while (index > _builder.Count) + { + _builder.Add(default(T)); + } + + if (index == _builder.Count) + { + _builder.Add(value); + } + else + { + _builder[index] = value; + } + } + + public void Add(T item) + { + _builder.Add(item); + } + + public void Insert(int index, T item) + { + _builder.Insert(index, item); + } + + public void EnsureCapacity(int capacity) + { + if (_builder.Capacity < capacity) + { + _builder.Capacity = capacity; + } + } + + public void Clear() + { + _builder.Clear(); + } + + public bool Contains(T item) + { + return _builder.Contains(item); + } + + public int IndexOf(T item) + { + return _builder.IndexOf(item); + } + + public int IndexOf(T item, IEqualityComparer<T> equalityComparer) + { + return _builder.IndexOf(item, 0, _builder.Count, equalityComparer); + } + + public int IndexOf(T item, int startIndex, int count) + { + return _builder.IndexOf(item, startIndex, count); + } + + public int FindIndex(Predicate<T> match) + => FindIndex(0, this.Count, match); + + public int FindIndex(int startIndex, Predicate<T> match) + => FindIndex(startIndex, this.Count - startIndex, match); + + public int FindIndex(int startIndex, int count, Predicate<T> match) + { + int endIndex = startIndex + count; + for (int i = startIndex; i < endIndex; i++) + { + if (match(_builder[i])) + { + return i; + } + } + + return -1; + } + + public void RemoveAt(int index) + { + _builder.RemoveAt(index); + } + + public void RemoveLast() + { + _builder.RemoveAt(_builder.Count - 1); + } + + public void ReverseContents() + { + _builder.Reverse(); + } + + public void Sort() + { + _builder.Sort(); + } + + public void Sort(IComparer<T> comparer) + { + _builder.Sort(comparer); + } + + public void Sort(Comparison<T> compare) + => Sort(Comparer<T>.Create(compare)); + + public void Sort(int startIndex, IComparer<T> comparer) + { + _builder.Sort(startIndex, _builder.Count - startIndex, comparer); + } + + public T[] ToArray() + { + return _builder.ToArray(); + } + + public void CopyTo(T[] array, int start) + { + _builder.CopyTo(array, start); + } + + public T Last() + { + return _builder[_builder.Count - 1]; + } + + public T First() + { + return _builder[0]; + } + + public bool Any() + { + return _builder.Count > 0; + } + + /// <summary> + /// Realizes the array. + /// </summary> + public ImmutableArray<T> ToImmutableOrNull() + { + if (Count == 0) + { + return default(ImmutableArray<T>); + } + + return this.ToImmutable(); + } + + /// <summary> + /// Realizes the array, downcasting each element to a derived type. + /// </summary> + public ImmutableArray<U> ToDowncastedImmutable<U>() + where U : T + { + if (Count == 0) + { + return ImmutableArray<U>.Empty; + } + + var tmp = ArrayBuilder<U>.GetInstance(Count); + foreach (var i in this) + { + tmp.Add((U)i); + } + + return tmp.ToImmutableAndFree(); + } + + /// <summary> + /// Realizes the array and disposes the builder in one operation. + /// </summary> + public ImmutableArray<T> ToImmutableAndFree() + { + var result = this.ToImmutable(); + this.Free(); + return result; + } + + public T[] ToArrayAndFree() + { + var result = this.ToArray(); + this.Free(); + return result; + } + + #region Poolable + + // To implement Poolable, you need two things: + // 1) Expose Freeing primitive. + public void Free() + { + var pool = _pool; + if (pool != null) + { + // According to the statistics of a C# compiler self-build, the most commonly used builder size is 0. (808003 uses). + // The distant second is the Count == 1 (455619), then 2 (106362) ... + // After about 50 (just 67) we have a long tail of infrequently used builder sizes. + // However we have builders with size up to 50K (just one such thing) + // + // We do not want to retain (potentially indefinitely) very large builders + // while the chance that we will need their size is diminishingly small. + // It makes sense to constrain the size to some "not too small" number. + // Overall perf does not seem to be very sensitive to this number, so I picked 128 as a limit. + if (this.Count < 128) + { + if (this.Count != 0) + { + this.Clear(); + } + + pool.Free(this); + return; + } + else + { + pool.ForgetTrackedObject(this); + } + } + } + + // 2) Expose the pool or the way to create a pool or the way to get an instance. + // for now we will expose both and figure which way works better + private static readonly ObjectPool<ArrayBuilder<T>> s_poolInstance = CreatePool(); + public static ArrayBuilder<T> GetInstance() + { + var builder = s_poolInstance.Allocate(); + Debug.Assert(builder.Count == 0); + return builder; + } + + public static ArrayBuilder<T> GetInstance(int capacity) + { + var builder = GetInstance(); + builder.EnsureCapacity(capacity); + return builder; + } + + public static ArrayBuilder<T> GetInstance(int capacity, T fillWithValue) + { + var builder = GetInstance(); + builder.EnsureCapacity(capacity); + + for (int i = 0; i < capacity; i++) + { + builder.Add(fillWithValue); + } + + return builder; + } + + public static ObjectPool<ArrayBuilder<T>> CreatePool() + { + return CreatePool(128); // we rarely need more than 10 + } + + public static ObjectPool<ArrayBuilder<T>> CreatePool(int size) + { + ObjectPool<ArrayBuilder<T>> pool = null; + pool = new ObjectPool<ArrayBuilder<T>>(() => new ArrayBuilder<T>(pool), size); + return pool; + } + + #endregion + + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + IEnumerator<T> IEnumerable<T>.GetEnumerator() + { + return GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal Dictionary<K, ImmutableArray<T>> ToDictionary<K>(Func<T, K> keySelector, IEqualityComparer<K> comparer = null) + { + if (this.Count == 1) + { + var dictionary1 = new Dictionary<K, ImmutableArray<T>>(1, comparer); + T value = this[0]; + dictionary1.Add(keySelector(value), ImmutableArray.Create(value)); + return dictionary1; + } + + if (this.Count == 0) + { + return new Dictionary<K, ImmutableArray<T>>(comparer); + } + + // bucketize + // prevent reallocation. it may not have 'count' entries, but it won't have more. + var accumulator = new Dictionary<K, ArrayBuilder<T>>(Count, comparer); + for (int i = 0; i < Count; i++) + { + var item = this[i]; + var key = keySelector(item); + if (!accumulator.TryGetValue(key, out var bucket)) + { + bucket = ArrayBuilder<T>.GetInstance(); + accumulator.Add(key, bucket); + } + + bucket.Add(item); + } + + var dictionary = new Dictionary<K, ImmutableArray<T>>(accumulator.Count, comparer); + + // freeze + foreach (var pair in accumulator) + { + dictionary.Add(pair.Key, pair.Value.ToImmutableAndFree()); + } + + return dictionary; + } + + public void AddRange(ArrayBuilder<T> items) + { + _builder.AddRange(items._builder); + } + + public void AddRange<U>(ArrayBuilder<U> items) where U : T + { + _builder.AddRange(items._builder); + } + + public void AddRange(ImmutableArray<T> items) + { + _builder.AddRange(items); + } + + public void AddRange(ImmutableArray<T> items, int length) + { + _builder.AddRange(items, length); + } + + public void AddRange<S>(ImmutableArray<S> items) where S : class, T + { + AddRange(ImmutableArray<T>.CastUp(items)); + } + + public void AddRange(T[] items, int start, int length) + { + for (int i = start, end = start + length; i < end; i++) + { + Add(items[i]); + } + } + + public void AddRange(IEnumerable<T> items) + { + _builder.AddRange(items); + } + + public void AddRange(params T[] items) + { + _builder.AddRange(items); + } + + public void AddRange(T[] items, int length) + { + _builder.AddRange(items, length); + } + + public void Clip(int limit) + { + Debug.Assert(limit <= Count); + _builder.Count = limit; + } + + public void ZeroInit(int count) + { + _builder.Clear(); + _builder.Count = count; + } + + public void AddMany(T item, int count) + { + for (int i = 0; i < count; i++) + { + Add(item); + } + } + + public void RemoveDuplicates() + { + var set = PooledHashSet<T>.GetInstance(); + + int j = 0; + for (int i = 0; i < Count; i++) + { + if (set.Add(this[i])) + { + this[j] = this[i]; + j++; + } + } + + Clip(j); + set.Free(); + } + + public ImmutableArray<S> SelectDistinct<S>(Func<T, S> selector) + { + var result = ArrayBuilder<S>.GetInstance(Count); + var set = PooledHashSet<S>.GetInstance(); + + foreach (var item in this) + { + var selected = selector(item); + if (set.Add(selected)) + { + result.Add(selected); + } + } + + set.Free(); + return result.ToImmutableAndFree(); + } + } +} diff --git a/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs b/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs new file mode 100644 index 0000000..2aa6e92 --- /dev/null +++ b/src/Text/Util/TextDataUtil/PooledObjects/ObjectPool`1.cs @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// define TRACE_LEAKS to get additional diagnostics that can lead to the leak sources. note: it will +// make everything about 2-3x slower +// +// #define TRACE_LEAKS + +// define DETECT_LEAKS to detect possible leaks +// #if DEBUG +// #define DETECT_LEAKS //for now always enable DETECT_LEAKS in debug. +// #endif + +using System; +using System.Diagnostics; +using System.Threading; + +#if DETECT_LEAKS +using System.Runtime.CompilerServices; + +#endif +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Generic implementation of object pooling pattern with predefined pool size limit. The main + /// purpose is that limited number of frequently used objects can be kept in the pool for + /// further recycling. + /// + /// Notes: + /// 1) it is not the goal to keep all returned objects. Pool is not meant for storage. If there + /// is no space in the pool, extra returned objects will be dropped. + /// + /// 2) it is implied that if object was obtained from a pool, the caller will return it back in + /// a relatively short time. Keeping checked out objects for long durations is ok, but + /// reduces usefulness of pooling. Just new up your own. + /// + /// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. + /// Rationale: + /// If there is no intent for reusing the object, do not use pool - just use "new". + /// </summary> + internal class ObjectPool<T> where T : class + { + [DebuggerDisplay("{Value,nq}")] + private struct Element + { + internal T Value; + } + + /// <remarks> + /// Not using System.Func{T} because this file is linked into the (debugger) Formatter, + /// which does not have that type (since it compiles against .NET 2.0). + /// </remarks> + internal delegate T Factory(); + + // Storage for the pool objects. The first item is stored in a dedicated field because we + // expect to be able to satisfy most requests from it. + private T _firstItem; + private readonly Element[] _items; + + // factory is stored for the lifetime of the pool. We will call this only when pool needs to + // expand. compared to "new T()", Func gives more flexibility to implementers and faster + // than "new T()". + private readonly Factory _factory; + +#if DETECT_LEAKS + private static readonly ConditionalWeakTable<T, LeakTracker> leakTrackers = new ConditionalWeakTable<T, LeakTracker>(); + + private class LeakTracker : IDisposable + { + private volatile bool disposed; + +#if TRACE_LEAKS + internal volatile object Trace = null; +#endif + + public void Dispose() + { + disposed = true; + GC.SuppressFinalize(this); + } + + private string GetTrace() + { +#if TRACE_LEAKS + return Trace == null ? "" : Trace.ToString(); +#else + return "Leak tracing information is disabled. Define TRACE_LEAKS on ObjectPool`1.cs to get more info \n"; +#endif + } + + ~LeakTracker() + { + if (!this.disposed && !Environment.HasShutdownStarted) + { + var trace = GetTrace(); + + // If you are seeing this message it means that object has been allocated from the pool + // and has not been returned back. This is not critical, but turns pool into rather + // inefficient kind of "new". + Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nPool detected potential leaking of {typeof(T)}. \n Location of the leak: \n {GetTrace()} TRACEOBJECTPOOLLEAKS_END"); + } + } + } +#endif + + internal ObjectPool(Factory factory) + : this(factory, Environment.ProcessorCount * 2) + { } + + internal ObjectPool(Factory factory, int size) + { + Debug.Assert(size >= 1); + _factory = factory; + _items = new Element[size - 1]; + } + + private T CreateInstance() + { + var inst = _factory(); + return inst; + } + + /// <summary> + /// Produces an instance. + /// </summary> + /// <remarks> + /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. + /// Note that Free will try to store recycled objects close to the start thus statistically + /// reducing how far we will typically search. + /// </remarks> + internal T Allocate() + { + // PERF: Examine the first element. If that fails, AllocateSlow will look at the remaining elements. + // Note that the initial read is optimistically not synchronized. That is intentional. + // We will interlock only when we have a candidate. in a worst case we may miss some + // recently returned objects. Not a big deal. + T inst = _firstItem; + if (inst == null || inst != Interlocked.CompareExchange(ref _firstItem, null, inst)) + { + inst = AllocateSlow(); + } + +#if DETECT_LEAKS + var tracker = new LeakTracker(); + leakTrackers.Add(inst, tracker); + +#if TRACE_LEAKS + var frame = CaptureStackTrace(); + tracker.Trace = frame; +#endif +#endif + return inst; + } + + private T AllocateSlow() + { + var items = _items; + + for (int i = 0; i < items.Length; i++) + { + // Note that the initial read is optimistically not synchronized. That is intentional. + // We will interlock only when we have a candidate. in a worst case we may miss some + // recently returned objects. Not a big deal. + T inst = items[i].Value; + if (inst != null) + { + if (inst == Interlocked.CompareExchange(ref items[i].Value, null, inst)) + { + return inst; + } + } + } + + return CreateInstance(); + } + + /// <summary> + /// Returns objects to the pool. + /// </summary> + /// <remarks> + /// Search strategy is a simple linear probing which is chosen for it cache-friendliness. + /// Note that Free will try to store recycled objects close to the start thus statistically + /// reducing how far we will typically search in Allocate. + /// </remarks> + internal void Free(T obj) + { + Validate(obj); + ForgetTrackedObject(obj); + + if (_firstItem == null) + { + // Intentionally not using interlocked here. + // In a worst case scenario two objects may be stored into same slot. + // It is very unlikely to happen and will only mean that one of the objects will get collected. + _firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + private void FreeSlow(T obj) + { + var items = _items; + for (int i = 0; i < items.Length; i++) + { + if (items[i].Value == null) + { + // Intentionally not using interlocked here. + // In a worst case scenario two objects may be stored into same slot. + // It is very unlikely to happen and will only mean that one of the objects will get collected. + items[i].Value = obj; + break; + } + } + } + + /// <summary> + /// Removes an object from leak tracking. + /// + /// This is called when an object is returned to the pool. It may also be explicitly + /// called if an object allocated from the pool is intentionally not being returned + /// to the pool. This can be of use with pooled arrays if the consumer wants to + /// return a larger array to the pool than was originally allocated. + /// </summary> + [Conditional("DEBUG")] + internal void ForgetTrackedObject(T old, T replacement = null) + { +#if DETECT_LEAKS + LeakTracker tracker; + if (leakTrackers.TryGetValue(old, out tracker)) + { + tracker.Dispose(); + leakTrackers.Remove(old); + } + else + { + var trace = CaptureStackTrace(); + Debug.WriteLine($"TRACEOBJECTPOOLLEAKS_BEGIN\nObject of type {typeof(T)} was freed, but was not from pool. \n Callstack: \n {trace} TRACEOBJECTPOOLLEAKS_END"); + } + + if (replacement != null) + { + tracker = new LeakTracker(); + leakTrackers.Add(replacement, tracker); + } +#endif + } + +#if DETECT_LEAKS + private static Lazy<Type> _stackTraceType = new Lazy<Type>(() => Type.GetType("System.Diagnostics.StackTrace")); + + private static object CaptureStackTrace() + { + return Activator.CreateInstance(_stackTraceType.Value); + } +#endif + + [Conditional("DEBUG")] + private void Validate(object obj) + { + Debug.Assert(obj != null, "freeing null?"); + + Debug.Assert(_firstItem != obj, "freeing twice?"); + + var items = _items; + for (int i = 0; i < items.Length; i++) + { + var value = items[i].Value; + if (value == null) + { + return; + } + + Debug.Assert(value != obj, "freeing twice?"); + } + } + } +} diff --git a/src/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs b/src/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs new file mode 100644 index 0000000..a98addd --- /dev/null +++ b/src/Text/Util/TextDataUtil/PooledObjects/PooledDictionary.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + // Dictionary that can be recycled via an object pool + // NOTE: these dictionaries always have the default comparer. + internal class PooledDictionary<K, V> : Dictionary<K, V> + { + private readonly ObjectPool<PooledDictionary<K, V>> _pool; + + private PooledDictionary(ObjectPool<PooledDictionary<K, V>> pool) + { + _pool = pool; + } + + public ImmutableDictionary<K, V> ToImmutableDictionaryAndFree() + { + var result = this.ToImmutableDictionary(); + this.Free(); + return result; + } + + public void Free() + { + this.Clear(); + _pool?.Free(this); + } + + // global pool + private static readonly ObjectPool<PooledDictionary<K, V>> s_poolInstance = CreatePool(); + + // if someone needs to create a pool; + public static ObjectPool<PooledDictionary<K, V>> CreatePool() + { + ObjectPool<PooledDictionary<K, V>> pool = null; + pool = new ObjectPool<PooledDictionary<K, V>>(() => new PooledDictionary<K, V>(pool), 128); + return pool; + } + + public static PooledDictionary<K, V> GetInstance() + { + var instance = s_poolInstance.Allocate(); + Debug.Assert(instance.Count == 0); + return instance; + } + } +} diff --git a/src/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs b/src/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs new file mode 100644 index 0000000..2969170 --- /dev/null +++ b/src/Text/Util/TextDataUtil/PooledObjects/PooledHashSet.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + // HashSet that can be recycled via an object pool + // NOTE: these HashSets always have the default comparer. + internal class PooledHashSet<T> : HashSet<T> + { + private readonly ObjectPool<PooledHashSet<T>> _pool; + + private PooledHashSet(ObjectPool<PooledHashSet<T>> pool) + { + _pool = pool; + } + + public void Free() + { + this.Clear(); + _pool?.Free(this); + } + + // global pool + private static readonly ObjectPool<PooledHashSet<T>> s_poolInstance = CreatePool(); + + // if someone needs to create a pool; + public static ObjectPool<PooledHashSet<T>> CreatePool() + { + ObjectPool<PooledHashSet<T>> pool = null; + pool = new ObjectPool<PooledHashSet<T>>(() => new PooledHashSet<T>(pool), 128); + return pool; + } + + public static PooledHashSet<T> GetInstance() + { + var instance = s_poolInstance.Allocate(); + Debug.Assert(instance.Count == 0); + return instance; + } + } +} diff --git a/src/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs b/src/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs new file mode 100644 index 0000000..b618095 --- /dev/null +++ b/src/Text/Util/TextDataUtil/PooledObjects/PooledStringBuilder.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.Text; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// The usage is: + /// var inst = PooledStringBuilder.GetInstance(); + /// var sb = inst.builder; + /// ... Do Stuff... + /// ... sb.ToString() ... + /// inst.Free(); + /// </summary> + internal class PooledStringBuilder + { + public readonly StringBuilder Builder = new StringBuilder(); + private readonly ObjectPool<PooledStringBuilder> _pool; + + private PooledStringBuilder(ObjectPool<PooledStringBuilder> pool) + { + Debug.Assert(pool != null); + _pool = pool; + } + + public int Length + { + get { return this.Builder.Length; } + } + + public void Free() + { + var builder = this.Builder; + + // do not store builders that are too large. + if (builder.Capacity <= 1024) + { + builder.Clear(); + _pool.Free(this); + } + else + { + _pool.ForgetTrackedObject(this); + } + } + + [System.Obsolete("Consider calling ToStringAndFree instead.")] + public new string ToString() + { + return this.Builder.ToString(); + } + + public string ToStringAndFree() + { + string result = this.Builder.ToString(); + this.Free(); + + return result; + } + + public string ToStringAndFree(int startIndex, int length) + { + string result = this.Builder.ToString(startIndex, length); + this.Free(); + + return result; + } + + // global pool + private static readonly ObjectPool<PooledStringBuilder> s_poolInstance = CreatePool(); + + // if someone needs to create a private pool; + public static ObjectPool<PooledStringBuilder> CreatePool() + { + ObjectPool<PooledStringBuilder> pool = null; + pool = new ObjectPool<PooledStringBuilder>(() => new PooledStringBuilder(pool), 32); + return pool; + } + + public static PooledStringBuilder GetInstance() + { + var builder = s_poolInstance.Allocate(); + Debug.Assert(builder.Builder.Length == 0); + return builder; + } + + public static implicit operator StringBuilder(PooledStringBuilder obj) + { + return obj.Builder; + } + } +} diff --git a/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs b/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs new file mode 100644 index 0000000..951cf14 --- /dev/null +++ b/src/Text/Util/TextDataUtil/ProjectionSpanDiffer.cs @@ -0,0 +1,287 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using Microsoft.VisualStudio.Text.Differencing; + using Microsoft.VisualStudio.Text.Projection; + + internal class ProjectionSpanDiffer + { + private IDifferenceService diffService; + + private ReadOnlyCollection<SnapshotSpan> inputDeletedSnapSpans; + private ReadOnlyCollection<SnapshotSpan> inputInsertedSnapSpans; + private bool computed; + + // exposed to unit tests + internal List<SnapshotSpan>[] deletedSurrogates; + internal List<SnapshotSpan>[] insertedSurrogates; + + public static ProjectionSpanDifference DiffSourceSpans(IDifferenceService diffService, + IProjectionSnapshot left, + IProjectionSnapshot right) + { + if (left == null) + { + throw new ArgumentNullException("left"); + } + if (right == null) + { + throw new ArgumentNullException("right"); + } + + if (!object.ReferenceEquals(left.TextBuffer, right.TextBuffer)) + { + throw new ArgumentException("left does not belong to the same text buffer as right"); + } + + ProjectionSpanDiffer differ = new ProjectionSpanDiffer + (diffService, + left.GetSourceSpans(), + right.GetSourceSpans()); + + return new ProjectionSpanDifference(differ.GetDifferences(), differ.InsertedSpans, differ.DeletedSpans); + } + + public ProjectionSpanDiffer(IDifferenceService diffService, + ReadOnlyCollection<SnapshotSpan> deletedSnapSpans, + ReadOnlyCollection<SnapshotSpan> insertedSnapSpans) + { + this.diffService = diffService; + this.inputDeletedSnapSpans = deletedSnapSpans; + this.inputInsertedSnapSpans = insertedSnapSpans; + } + + public ReadOnlyCollection<SnapshotSpan> DeletedSpans { get; private set; } + public ReadOnlyCollection<SnapshotSpan> InsertedSpans { get; private set; } + + public IDifferenceCollection<SnapshotSpan> GetDifferences() + { + if (!this.computed) + { + DecomposeSpans(); + this.computed = true; + } + + var deletedSpans = new List<SnapshotSpan>(); + var insertedSpans = new List<SnapshotSpan>(); + + DeletedSpans = deletedSpans.AsReadOnly(); + InsertedSpans = insertedSpans.AsReadOnly(); + + for (int s = 0; s < deletedSurrogates.Length; ++s) + { + deletedSpans.AddRange(deletedSurrogates[s]); + } + + for (int s = 0; s < insertedSurrogates.Length; ++s) + { + insertedSpans.AddRange(insertedSurrogates[s]); + } + + return this.diffService.DifferenceSequences(deletedSpans, insertedSpans); + } + + #region Internal (for testing) helpers + internal void DecomposeSpans() + { + // Prepare spans for diffing. The basic idea is this: suppose we have input spans from some source snapshot as follows: + // + // deleted: 0..10 + // inserted: 0..3 7..10 + // + // We would like to raise a text change event that indicates that the text from 3..7 was deleted, rather than + // an event indicating that all the text from 0..10 was deleted and replaced. We could simply compute a string + // difference of the before & after text, but there might be a lot of text (so that would be expensive), and we + // also don't want to suppress eventing when identical text comes from different source buffers (which might have + // different content types). So, this routine converts the input spans into a form suitable for diffing: + // + // deleted: 0..3 3..7 7..10 + // inserted 0..3 7..10 + // + // then we compute the differences of the spans qua spans, which in this case will indicate that the span named "3..7" + // was deleted, and that's what we use to generate text change events. + + // what to substitute for input spans during diffing + this.deletedSurrogates = new List<SnapshotSpan>[this.inputDeletedSnapSpans.Count]; + this.insertedSurrogates = new List<SnapshotSpan>[this.inputInsertedSnapSpans.Count]; + + // collect spans by text buffer + + Dictionary<ITextSnapshot, List<Thing>> buffer2DeletedThings = new Dictionary<ITextSnapshot, List<Thing>>(); + for (int ds = 0; ds < this.inputDeletedSnapSpans.Count; ++ds) + { + SnapshotSpan ss = this.inputDeletedSnapSpans[ds]; + List<Thing> things; + if (!buffer2DeletedThings.TryGetValue(ss.Snapshot, out things)) + { + things = new List<Thing>(); + buffer2DeletedThings.Add(ss.Snapshot, things); + } + things.Add(new Thing(ss.Span, ds)); + + // unrelated + deletedSurrogates[ds] = new List<SnapshotSpan>(); + } + + Dictionary<ITextSnapshot, List<Thing>> buffer2InsertedThings = new Dictionary<ITextSnapshot, List<Thing>>(); + for (int ns = 0; ns < this.inputInsertedSnapSpans.Count; ++ns) + { + SnapshotSpan ss = this.inputInsertedSnapSpans[ns]; + List<Thing> things; + if (!buffer2InsertedThings.TryGetValue(ss.Snapshot, out things)) + { + things = new List<Thing>(); + buffer2InsertedThings.Add(ss.Snapshot, things); + } + things.Add(new Thing(ss.Span, ns)); + + // unrelated + insertedSurrogates[ns] = new List<SnapshotSpan>(); + } + + foreach (KeyValuePair<ITextSnapshot, List<Thing>> pair in buffer2DeletedThings) + { + List<Thing> insertedThings; + ITextSnapshot snapshot = pair.Key; + if (buffer2InsertedThings.TryGetValue(snapshot, out insertedThings)) + { + List<Thing> deletedThings = pair.Value; + insertedThings.Sort(Comparison); + deletedThings.Sort(Comparison); + + int i = 0; + int d = 0; + do + { + Span inserted = insertedThings[i].span; + Span deleted = deletedThings[d].span; + Span? overlap = inserted.Overlap(deleted); + + if (overlap == null) + { + if (inserted.Start < deleted.Start) + { + i++; + } + else + { + d++; + } + } + else + { + NormalizedSpanCollection insertedResidue = NormalizedSpanCollection.Difference(new NormalizedSpanCollection(inserted), + new NormalizedSpanCollection(overlap.Value)); // todo add overload to normalizedspancollection + if (insertedResidue.Count > 0) + { + int pos = insertedThings[i].position; + insertedThings.RemoveAt(i); + bool didOverlap = false; + int ir = 0; + while (ir < insertedResidue.Count) + { + Span r = insertedResidue[ir]; + if (didOverlap || r.Start < overlap.Value.Start) + { + insertedThings.Insert(i++, new Thing(r, pos)); + ir++; + } + else + { + insertedThings.Insert(i++, new Thing(overlap.Value, pos)); + didOverlap = true; + } + } + if (!didOverlap) + { + insertedThings.Insert(i++, new Thing(overlap.Value, pos)); + } + i--; + } + + NormalizedSpanCollection deletedResidue = NormalizedSpanCollection.Difference(new NormalizedSpanCollection(deleted), + new NormalizedSpanCollection(overlap.Value)); + if (deletedResidue.Count > 0) + { + int pos = deletedThings[d].position; + deletedThings.RemoveAt(d); + bool didOverlap = false; + int dr = 0; + while (dr < deletedResidue.Count) + { + Span r = deletedResidue[dr]; + if (didOverlap || r.Start < overlap.Value.Start) + { + deletedThings.Insert(d++, new Thing(r, pos)); + dr++; + } + else + { + deletedThings.Insert(d++, new Thing(overlap.Value, pos)); + didOverlap = true; + } + } + if (!didOverlap) + { + deletedThings.Insert(d++, new Thing(overlap.Value, pos)); + } + d--; + } + } + if (inserted.End <= deleted.End) + { + i++; + } + if (deleted.End <= inserted.End) + { + d++; + } + } while (i < insertedThings.Count && d < deletedThings.Count); + } + } + + foreach (KeyValuePair<ITextSnapshot, List<Thing>> pair in buffer2DeletedThings) + { + foreach (Thing t in pair.Value) + { + deletedSurrogates[t.position].Add(new SnapshotSpan(pair.Key, t.span)); + } + } + + foreach (KeyValuePair<ITextSnapshot, List<Thing>> pair in buffer2InsertedThings) + { + foreach (Thing t in pair.Value) + { + insertedSurrogates[t.position].Add(new SnapshotSpan(pair.Key, t.span)); + } + } + } + + #endregion + + #region Private helpers + private class Thing + { + public Span span; + public int position; + public Thing(Span span, int position) + { + this.span = span; + this.position = position; + } + } + + private int Comparison(Thing left, Thing right) + { + return left.span.Start - right.span.Start; + } + #endregion + + } +} diff --git a/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs b/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs new file mode 100644 index 0000000..99d3267 --- /dev/null +++ b/src/Text/Util/TextDataUtil/ProjectionSpanDifference.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System.Collections.ObjectModel; +using Microsoft.VisualStudio.Text.Differencing; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Represents the set of differences between two projection snapshots. + /// </summary> + public class ProjectionSpanDifference + { + /// <summary> + /// Initializes a new instance of <see cref="ProjectionSpanDifference"/>. + /// </summary> + /// <param name="differenceCollection">The collection of snapshot spans that include the differences.</param> + /// <param name="insertedSpans">A read-only collection of the inserted snapshot spans.</param> + /// <param name="deletedSpans">A read-only collection of the deleted snapshot spans.</param> + public ProjectionSpanDifference(IDifferenceCollection<SnapshotSpan> differenceCollection, ReadOnlyCollection<SnapshotSpan> insertedSpans, ReadOnlyCollection<SnapshotSpan> deletedSpans) + { + DifferenceCollection = differenceCollection; + InsertedSpans = insertedSpans; + DeletedSpans = deletedSpans; + } + + /// <summary> + /// The collection of differences between the two snapshots. + /// </summary> + public IDifferenceCollection<SnapshotSpan> DifferenceCollection { get; private set; } + + /// <summary> + /// The read-only collection of inserted snapshot spans. + /// </summary> + public ReadOnlyCollection<SnapshotSpan> InsertedSpans { get; private set; } + + /// <summary> + /// The read-only collection of deleted snapshot spans. + /// </summary> + public ReadOnlyCollection<SnapshotSpan> DeletedSpans { get; private set; } + } +} diff --git a/src/Text/Util/TextDataUtil/SnapshotTracker.cs b/src/Text/Util/TextDataUtil/SnapshotTracker.cs new file mode 100644 index 0000000..e89bf02 --- /dev/null +++ b/src/Text/Util/TextDataUtil/SnapshotTracker.cs @@ -0,0 +1,54 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + + /// <summary> + /// Tracks the extant snapshots of a text buffer for diagnostic purposes. + /// </summary> + internal sealed class SnapshotTracker : IDisposable + { + private readonly ITextBuffer textBuffer; + List<WeakReference> snapshots = new List<WeakReference>(); + + public SnapshotTracker(ITextBuffer textBuffer) + { + this.textBuffer = textBuffer; + this.snapshots.Add(new WeakReference(textBuffer.CurrentSnapshot)); + textBuffer.ChangedLowPriority += OnTextContentChanged; + } + + public void Dispose() + { + this.textBuffer.ChangedLowPriority -= OnTextContentChanged; + GC.SuppressFinalize(this); + } + + private void OnTextContentChanged(object sender, TextContentChangedEventArgs e) + { + this.snapshots.Add(new WeakReference(e.After)); + } + + public int ReportLiveSnapshots(System.IO.TextWriter writer) + { + int liveSnapshots = 0; + writer.Write(" Snapshots: "); + for (int s = 0; s < this.snapshots.Count; ++s) + { + object target = snapshots[s].Target; + if (target != null) + { + liveSnapshots++; + ITextVersion v = ((ITextSnapshot)target).Version; + writer.Write(string.Format(System.Globalization.CultureInfo.CurrentCulture, "{0}:{1} ", v.VersionNumber, v.Length)); + } + } + writer.WriteLine(); + return liveSnapshots; + } + } +}
\ No newline at end of file diff --git a/src/Text/Util/TextDataUtil/TextModelOptions.cs b/src/Text/Util/TextDataUtil/TextModelOptions.cs new file mode 100644 index 0000000..1863bfd --- /dev/null +++ b/src/Text/Util/TextDataUtil/TextModelOptions.cs @@ -0,0 +1,22 @@ +// +// 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.Utilities +{ + public static class TextModelOptions + { + // these values should be set by MEF component in editor options component. defaults + // are here just in case that doesn't happen in some configuration. + public static int CompressedStorageFileSizeThreshold = 5 * 1024 * 1024; // 5 MB file (typically 10 MB in memory) + public static int CompressedStoragePageSize = 1 * 1024 * 1024; // 1 MB per page (so 10 pages at the low end) + public static int CompressedStorageMaxLoadedPages = 3; // at most 3 pages loaded + public static bool CompressedStorageGlobalManagement = false; // per document + public static bool CompressedStorageRetainWeakReferences = true; // forces worst case decompression for testing purposes + + public static int StringRebuilderMaxCharactersToConsolidate = 200; // Combine adjacent pieces when sum of sizes is less than this and + public static int StringRebuilderMaxLinesToConsolidate = 8; // Combine adjacent pieces when number of lines is less than this + + public static int DiffSizeThreshold = 25 * 1024 * 1024; // threshold above which to do poor man's diff + } +} diff --git a/src/Text/Util/TextDataUtil/TextUtilities.cs b/src/Text/Util/TextDataUtil/TextUtilities.cs new file mode 100644 index 0000000..26aa19a --- /dev/null +++ b/src/Text/Util/TextDataUtil/TextUtilities.cs @@ -0,0 +1,438 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using System.Text; + using System.Diagnostics; + using System.Threading.Tasks; + + /// <summary> + /// Handy text-oriented utilities. + /// </summary> + internal static class TextUtilities + { + public static readonly Task CompletedNonInliningTask = CreateCompletedNonInliningTask(); + + private static Task CreateCompletedNonInliningTask() + { + var tcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously); + tcs.SetResult(null); + return tcs.Task; + } + + public static Span? Overlap(this Span span, Span? otherSpan) + { + return otherSpan != null ? span.Overlap(otherSpan.Value) : null; + } + + public static int LengthOfLineBreak(string source, int start, int end) + { + char c1 = source[start]; + if (c1 == '\r') + { + return ((++start < end) && (source[start] == '\n')) ? 2 : 1; + } + else if ((c1 == '\n') || (c1 == '\u0085') || + (c1 == '\u2028' /*unicode line separator*/) || + (c1 == '\u2029' /*unicode paragraph separator*/)) + { + return 1; + } + else + return 0; + } + + public static int LengthOfLineBreak(char[] source, int start, int end) + { + char c1 = source[start]; + if (c1 == '\r') + { + return ((++start < end) && (source[start] == '\n')) ? 2 : 1; + } + else if ((c1 == '\n') || (c1 == '\u0085') || + (c1 == '\u2028' /*unicode line separator*/) || + (c1 == '\u2029' /*unicode paragraph separator*/)) + { + return 1; + } + else + return 0; + } + + /// <summary> + /// A utility function that returns the number of lines in a given block of + /// text + /// + /// We honor the MAC, UNIX and PC way of line counting. + /// + /// 0x2028, 0x2929, 0x85, \r, \n and \r\n are considered as valid line breaks. However, \n\r is not + /// (that is to say, \n\r comprises TWO line breaks). + /// </summary> + static public int ScanForLineCount(string text) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + int lines = 0; + for (int i = 0; i < text.Length; ) + { + int breakLength = LengthOfLineBreak(text, i, text.Length); + if (breakLength > 0) + { + ++lines; + i += breakLength; + } + else + ++i; + } + + return lines; + } + + /// <summary> + /// A utility function that returns the number of lines in a given block of text. + /// + /// We honor the MAC, UNIX and PC way of line counting. + /// + /// 0x2028, 0x2929, 0x85, \r, \n and \r\n are considered as valid line breaks. However, \n\r is not + /// (that is to say, \n\r comprises TWO line breaks). + /// </summary> + static public int ScanForLineCount(char[] text, int start, int length) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + int lines = 0; + for (int i = 0; i < length; ) + { + int breakLength = LengthOfLineBreak(text, start + i, length); + if (breakLength > 0) + { + ++lines; + i += breakLength; + } + else + ++i; + } + + return lines; + } + + /// <summary> + /// Perform stable merge sort on <paramref name="elements"/>. + /// </summary> + /// <param name="elements">List of elements to be sorted.</param> + /// <param name="comparer">He who knows how to compare elements.</param> + /// <returns>Array of parameter T sorted in ascending order.</returns> + static public T[] StableSort<T>(IReadOnlyList<T> elements, Comparison<T> comparer) + { + int count = elements.Count; + T[] target = new T[count]; + bool alreadySorted = true; + if (count > 0) + { + target[0] = elements[0]; + for (int i = 1; i < count; ++i) + { + target[i] = elements[i]; + // check for already-sorted array; quite likely in applicable text editor scenarios + if (comparer(target[i - 1], target[i]) > 0) + { + alreadySorted = false; + } + } + } + if (alreadySorted) + { + return target; + } + else + { + // do a merge sort + + T[] source = new T[count]; + int subcount = 1; // the length of the subsequences to merge + while (subcount < count) + { + T[] t = source; + source = target; + target = t; + + MergePass(source, target, subcount, comparer); + + subcount *= 2; + } +#if DEBUG + // trust, but verify + for (int i = 1; i < count; ++i) + { + System.Diagnostics.Debug.Assert(comparer(target[i - 1], target[i]) <= 0); + } +#endif + return target; + } + } + + /// <summary> + /// Make one pass over <paramref name="source"/>, merging runs of size <paramref name="subcount"/> into <paramref name="target"/>. + /// </summary> + /// <param name="source">Array containing sorted runs of size <paramref name="subcount"/>.</param> + /// <param name="target">Array into which runs of size 2 * <paramref name="subcount"/> will be placed.</param> + /// <param name="subcount">Size of sorted runs that will be generated into <paramref name="target"/>.</param> + /// <param name="comparer">He who knows how to compare elements.</param> + static private void MergePass<T>(T[] source, T[] target, int subcount, Comparison<T> comparer) + { + int offset = 0; // offset of the pair of subsequences that are being merged + while (offset <= source.Length - 2 * subcount) + { + Merge(source, target, offset, offset + subcount, offset + 2 * subcount, comparer); + offset += 2 * subcount; + } + if (offset + subcount < source.Length) + { + // two subsequences remain, but the second one is smaller than subcount + Merge(source, target, offset, offset + subcount, source.Length, comparer); + } + else + { + // only one (already sorted) subsequence remains; copy it across + for (int i = offset; i < source.Length; ++i) + { + target[i] = source[i]; + } + } + } + + /// <summary> + /// Merge two ordered runs from <paramref name="source"/> array into <paramref name="target"/> array. + /// </summary> + /// <param name="source">Array containing input runs.</param> + /// <param name="target">Array into which output run is placed.</param> + /// <param name="left">Starting position of first source run.</param> + /// <param name="right">Starting position of second source run.</param> + /// <param name="end">First position past second source run.</param> + /// <param name="comparer">He who knows how to compare elements.</param> + static private void Merge<T>(T[] source, T[] target, int left, int right, int end, Comparison<T> comparer) + { + int s1 = left; + int s2 = right; + int t = left; + while (s1 < right && s2 < end) + { + target[t++] = (comparer(source[s1], source[s2]) <= 0) ? source[s1++] : source[s2++]; + } + if (s1 == right) + { + // copy any leftovers from second source run. + for (int i = t; i < end; ++i) + { + target[i] = source[s2++]; + } + } + else + { + // copy any leftovers from first source run. + for (int i = t; i < end; ++i) + { + target[i] = source[s1++]; + } + } + } + + /// <summary> + /// Associate a string-valued tag with the buffer for diagnostic purposes. + /// </summary> + /// <param name="buffer">The <see cref="ITextBuffer"/> of interest.</param> + /// <param name="tag">Informative tag to associate with the buffer.</param> + /// <exception cref="ArgumentNullException">if <paramref name="buffer"/> is null.</exception> + /// <exception cref="ArgumentNullException">if <paramref name="tag"/> is null.</exception> + /// <remarks>If the buffer already has a tag, the new tag is concatenated to it (with an intervening "::".</remarks> + public static void TagBuffer(ITextBuffer buffer, string tag) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + if (tag == null) + { + throw new ArgumentNullException("tag"); + } + string existingTag = ""; + if (!buffer.Properties.TryGetProperty<string>("tag", out existingTag)) + { + buffer.Properties.AddProperty("tag", tag); + } + else + { + buffer.Properties["tag"] = existingTag + "::" + tag; + } + } + + /// <summary> + /// Return the string-valued tag previously associated with the buffer by <see cref="TagBuffer"/>. + /// </summary> + /// <param name="buffer">The <see cref="ITextBuffer"/> of interest.</param> + /// <returns>The previously associated tag, or "" if no tag has been associated.</returns> + /// <exception cref="ArgumentNullException">if <paramref name="buffer"/> is null.</exception> + public static string GetTag(ITextBuffer buffer) + { + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + string tag = ""; + buffer.Properties.TryGetProperty<string>("tag", out tag); + return tag; + } + + public static string GetTagOrContentType(ITextBuffer buffer) + { + return GetTag(buffer) ?? buffer.ContentType.TypeName; + } + + public static string Escape(string text) + { + StringBuilder result = new StringBuilder(); + for (int c = 0; c < text.Length; ++c) + { + char ch = text[c]; + AppendChar(result, ch); + } + return result.ToString(); + } + + public static string Escape(string text, int truncateLength) + { + StringBuilder result = new StringBuilder(); + if (text.Length <= truncateLength) + { + return Escape(text); + } + else + { + for (int c = 0; c < truncateLength / 2; ++c) + { + char ch = text[c]; + AppendChar(result, ch); + } + result.Append(" � "); + for (int c = text.Length - (truncateLength / 2); c < text.Length; ++c) + { + char ch = text[c]; + AppendChar(result, ch); + } + } + return result.ToString(); + } + + private static void AppendChar(StringBuilder result, char ch) + { + switch (ch) + { + case '\\': + result.Append(@"\\"); + break; + case '\r': + result.Append(@"\r"); + break; + case '\n': + result.Append(@"\n"); + break; + case '\t': + result.Append(@"\t"); + break; + case '"': + result.Append("\\\""); + break; + default: + result.Append(ch); + break; + } + } + + internal static void RaiseEvent(object sender, EventHandler eventHandlers, IEnumerable<IExtensionErrorHandler> errorHandlers) + { + if (eventHandlers == null) + return; + + var handlers = eventHandlers.GetInvocationList(); + + foreach (EventHandler handler in handlers) + { + try + { + handler(sender, EventArgs.Empty); + } + catch (Exception e) + { + HandleException(sender, e, errorHandlers); + } + } + } + + internal static void RaiseEvent<TArgs>(object sender, EventHandler<TArgs> eventHandlers, TArgs args, + IEnumerable<IExtensionErrorHandler> errorHandlers) where TArgs : EventArgs + { + if (eventHandlers == null) + return; + + var handlers = eventHandlers.GetInvocationList(); + + foreach (EventHandler<TArgs> handler in handlers) + { + try + { + handler(sender, args); + } + catch (Exception e) + { + HandleException(sender, e, errorHandlers); + } + } + } + + internal static void CallExtensionPoint(object errorSource, Action call, IEnumerable<IExtensionErrorHandler> errorHandlers) + { + try + { + call(); + } + catch (Exception e) + { + HandleException(errorSource, e, errorHandlers); + } + } + + internal static void HandleException(object errorSource, Exception e, IEnumerable<IExtensionErrorHandler> errorHandlers) + { + bool handled = false; + if (errorHandlers != null) + { + foreach (var errorHandler in errorHandlers) + { + try + { + errorHandler.HandleError(errorSource, e); + handled = true; + } + catch (Exception doubleFaultException) + { + // TODO: What is the right behavior here? + Debug.Fail(doubleFaultException.ToString()); + } + } + } + if (!handled) + { + // TODO: What is the right behavior here? + Debug.Fail(e.ToString()); + } + } + } +} diff --git a/src/Text/Util/TextDataUtil/WeakReferenceForKey.cs b/src/Text/Util/TextDataUtil/WeakReferenceForKey.cs new file mode 100644 index 0000000..550218c --- /dev/null +++ b/src/Text/Util/TextDataUtil/WeakReferenceForKey.cs @@ -0,0 +1,102 @@ +// +// 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.Runtime.Serialization; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// This class is the same as WeakReference, except for its implementation of Object.GetHashCode() and Object.Equals(). + /// In both of those cases, it makes a best attempt at emulating the behavior of the target object, so that when + /// WeakReferences are placed within a dictionary, as key values, they are properly recognized as references to objects + /// that are the different (or the same). + /// </summary> + [Serializable] + public class WeakReferenceForDictionaryKey : WeakReference + { + private readonly int hashCode; + private const int xorWeakReference = 0xCCCC; + + public WeakReferenceForDictionaryKey(object target) + : base(target) + { + if (target != null) + hashCode = target.GetHashCode() ^ xorWeakReference; + } + + protected WeakReferenceForDictionaryKey(SerializationInfo info, StreamingContext context) : base(info, context) + { + return; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + return ; + } + + /// <summary> + /// This hash code is informed by (but not the same as) the target object, and is constant for the life + /// of the WeakReference. + /// </summary> + /// <returns>A hash code for the current Object.</returns> + public override int GetHashCode() + { + return hashCode; + } + + /// <summary> + /// Equality is trickier than the Hashcode, because of the possibility that the target of either WeakReference + /// has been GC'ed or finalized. In those cases, when it is not known that the references must have refered to + /// the same object, Equals() returns false. Otherwise, it returns the same as the underlying Equals in the + /// target objects. + /// </summary> + /// <param name="obj">The Object to compare with the current Object</param> + /// <returns>true if the specified Object is equal to the current Object; otherwise false.</returns> + public override bool Equals(object obj) + { + bool result = false; + WeakReferenceForDictionaryKey other = obj as WeakReferenceForDictionaryKey; + + if (Object.ReferenceEquals(other, null)) + { + result = false; + } + else if (Object.ReferenceEquals(this, other)) + { + result = true; + } + else + { + object thisObj = null; + object otherObj = null; + + try + { + thisObj = this.Target; + otherObj = other.Target; + } + catch (InvalidOperationException) + { + } + + if (thisObj == null || otherObj == null) + { + // these objects either refered to null (is that even possible?) or refer to + // something that has been GC'ed/finalized. the latter case is the important + // scenario for us, and we'll return false, in effect causing each GC'ed + // weak reference to appear to be equal only to itself. + result = false; + } + else + { + result = Object.Equals(thisObj, otherObj); + } + } + + return result; + } + } +}
\ No newline at end of file diff --git a/src/Text/Util/TextLogicUtil/INamedTaggerMetadata.cs b/src/Text/Util/TextLogicUtil/INamedTaggerMetadata.cs new file mode 100644 index 0000000..44130e0 --- /dev/null +++ b/src/Text/Util/TextLogicUtil/INamedTaggerMetadata.cs @@ -0,0 +1,19 @@ +// +// 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.Tagging +{ + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// The metadata interface for exporters and importers of metadata on <see cref="ITaggerProvider"/> factories. + /// </summary> + public interface INamedTaggerMetadata : ITaggerMetadata, INamedContentTypeMetadata + { + } +} diff --git a/src/Text/Util/TextLogicUtil/ITaggerMetadata.cs b/src/Text/Util/TextLogicUtil/ITaggerMetadata.cs new file mode 100644 index 0000000..468144b --- /dev/null +++ b/src/Text/Util/TextLogicUtil/ITaggerMetadata.cs @@ -0,0 +1,23 @@ +// +// 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.Tagging +{ + using System; + using System.Collections.Generic; + using System.Text; + using Microsoft.VisualStudio.Text.Utilities; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// The metadata interface for exporters and importers of metadata on <see cref="ITaggerProvider"/> factories. + /// </summary> + public interface ITaggerMetadata : IContentTypeMetadata + { + /// <summary> + /// The set of <see cref="TagTypeAttribute"/> objects. + /// </summary> + IEnumerable<Type> TagTypes { get; } + } +} diff --git a/src/Text/Util/TextLogicUtil/TextUndoPrimitive.cs b/src/Text/Util/TextLogicUtil/TextUndoPrimitive.cs new file mode 100644 index 0000000..b9e7f60 --- /dev/null +++ b/src/Text/Util/TextLogicUtil/TextUndoPrimitive.cs @@ -0,0 +1,51 @@ +// +// 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.Operations +{ + internal abstract class TextUndoPrimitive : ITextUndoPrimitive + { + private ITextUndoTransaction parent; + + public virtual ITextUndoTransaction Parent + { + get { return this.parent; } + set { this.parent = value; } + } + + public virtual bool CanRedo + { + get { return true; } + } + + public virtual bool CanUndo + { + get { return true; } + } + + protected virtual void Toggle() + { + } + + public virtual void Do() + { + Toggle(); + } + + public virtual void Undo() + { + Toggle(); + } + + public virtual bool CanMerge(ITextUndoPrimitive older) + { + return false; + } + + public virtual ITextUndoPrimitive Merge(ITextUndoPrimitive older) + { + throw new System.NotSupportedException(); + } + } +}
\ No newline at end of file diff --git a/src/Text/Util/TextUIUtil/IContentTypeAndTextViewRoleMetadata.cs b/src/Text/Util/TextUIUtil/IContentTypeAndTextViewRoleMetadata.cs new file mode 100644 index 0000000..b243fc7 --- /dev/null +++ b/src/Text/Util/TextUIUtil/IContentTypeAndTextViewRoleMetadata.cs @@ -0,0 +1,15 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using Microsoft.VisualStudio.Utilities; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Metadata which includes Content Types and Text View Roles + /// </summary> + public interface IContentTypeAndTextViewRoleMetadata : IContentTypeMetadata, ITextViewRoleMetadata + { + } +} diff --git a/src/Text/Util/TextUIUtil/IDeferrableContentTypeAndTextViewRoleMetadata.cs b/src/Text/Util/TextUIUtil/IDeferrableContentTypeAndTextViewRoleMetadata.cs new file mode 100644 index 0000000..8150cfd --- /dev/null +++ b/src/Text/Util/TextUIUtil/IDeferrableContentTypeAndTextViewRoleMetadata.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +using System.ComponentModel; + +namespace Microsoft.VisualStudio.Text.Utilities +{ + /// <summary> + /// Metadata which includes Content Types and Text View Roles + /// </summary> + public interface IDeferrableContentTypeAndTextViewRoleMetadata : IContentTypeAndTextViewRoleMetadata + { + /// <summary> + /// Optional OptionId that controls creation of the extension. + /// </summary> + [DefaultValue(null)] + string OptionName { get; } + } +} diff --git a/src/Text/Util/TextUIUtil/ITextViewRoleMetadata.cs b/src/Text/Util/TextUIUtil/ITextViewRoleMetadata.cs new file mode 100644 index 0000000..4307b0c --- /dev/null +++ b/src/Text/Util/TextUIUtil/ITextViewRoleMetadata.cs @@ -0,0 +1,13 @@ +// +// 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.Utilities +{ + using System.Collections.Generic; + + public interface ITextViewRoleMetadata + { + IEnumerable<string> TextViewRoles { get; } + } +}
\ No newline at end of file diff --git a/src/Text/Util/TextUIUtil/IWpfTextViewMarginMetadata.cs b/src/Text/Util/TextUIUtil/IWpfTextViewMarginMetadata.cs new file mode 100644 index 0000000..2f81035 --- /dev/null +++ b/src/Text/Util/TextUIUtil/IWpfTextViewMarginMetadata.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// +namespace Microsoft.VisualStudio.Text.Utilities +{ + using System.ComponentModel; + using System.Windows; + using System.Collections.Generic; + using Microsoft.VisualStudio.Utilities; + + public interface IWpfTextViewMarginMetadata : IOrderable, IContentTypeAndTextViewRoleMetadata + { + /// <summary> + /// Gets the name of the margin that contains this margin. + /// </summary> + string MarginContainer { get; } + + [DefaultValue(null)] + IEnumerable<string> Replaces { get; } + + /// <summary> + /// Optional OptionName that controls creation and visibility of the margin. + /// </summary> + [DefaultValue(null)] + string OptionName { get; } + + /// <summary> + /// Gets the grid unit type to be used for drawing of this element in the container margin's grid. + /// </summary> + [DefaultValue(GridUnitType.Auto)] + GridUnitType GridUnitType { get; } + + /// <summary> + /// Gets the size of the grid cell in which the margin should be placed. + /// </summary> + [DefaultValue(1.0)] + double GridCellLength { get; } + } +} diff --git a/src/Text/Util/TextUIUtil/UIExtensionSelector.cs b/src/Text/Util/TextUIUtil/UIExtensionSelector.cs new file mode 100644 index 0000000..3a1ba27 --- /dev/null +++ b/src/Text/Util/TextUIUtil/UIExtensionSelector.cs @@ -0,0 +1,83 @@ +// +// 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.Utilities +{ + using System; + using System.Collections.Generic; + using Microsoft.VisualStudio.Text.Editor; + using Microsoft.VisualStudio.Utilities; + + /// <summary> + /// Helper class to perform ContentType and TextViewRole match against a set of extensions. + /// </summary> + internal static class UIExtensionSelector + { + /// <summary> + /// Given a list of extensions that provide text view roles, filter the list and return that + /// subset which matches at least one of the roles in the provided set of roles. + /// </summary> + public static List<Lazy<TProvider, TMetadataView>> SelectMatchingExtensions<TProvider, TMetadataView> + (IEnumerable<Lazy<TProvider, TMetadataView>> providerHandles, + ITextViewRoleSet viewRoles) + where TMetadataView : ITextViewRoleMetadata // text view role is required + { + var result = new List<Lazy<TProvider, TMetadataView>>(); + foreach (var providerHandle in providerHandles) + { + IEnumerable<string> providerRoles = providerHandle.Metadata.TextViewRoles; + if (viewRoles.ContainsAny(providerRoles)) + { + result.Add(providerHandle); + } + } + return result; + } + + /// <summary> + /// Given a list of extensions that provide text view roles and content types, filter the list and return that + /// subset which matches the content types and at least one of the roles in the provided set of roles. + /// </summary> + public static List<Lazy<TProvider, TMetadataView>> SelectMatchingExtensions<TProvider, TMetadataView> + (IEnumerable<Lazy<TProvider, TMetadataView>> providerHandles, + IContentType documentContentType, + IContentType excludedContentType, + ITextViewRoleSet viewRoles) + where TMetadataView : IContentTypeAndTextViewRoleMetadata // both content type and text view role are required + { + var result = new List<Lazy<TProvider, TMetadataView>>(); + foreach (var providerHandle in providerHandles) + { + // first, check content type match + if ((excludedContentType == null || !ExtensionSelector.ContentTypeMatch(excludedContentType, providerHandle.Metadata.ContentTypes)) && + ExtensionSelector.ContentTypeMatch(documentContentType, providerHandle.Metadata.ContentTypes) && + viewRoles.ContainsAny(providerHandle.Metadata.TextViewRoles)) + { + result.Add(providerHandle); + } + } + return result; + } + + /// <summary> + /// Given a list of extensions that provide text view roles and content types, return the + /// instantiated extension which best matches the given content type and matches at least one of the roles. + /// </summary> + public static TExtensionInstance InvokeBestMatchingFactory<TExtensionInstance, TExtensionFactory, TMetadataView> + (IEnumerable<Lazy<TExtensionFactory, TMetadataView>> providerHandles, + IContentType dataContentType, + ITextViewRoleSet viewRoles, + Func<TExtensionFactory, TExtensionInstance> getter, + IContentTypeRegistryService contentTypeRegistryService, + GuardedOperations guardedOperations, + object errorSource) + where TMetadataView : IContentTypeAndTextViewRoleMetadata // both content type and text view role are required + where TExtensionFactory : class + where TExtensionInstance : class + { + var roleMatchingProviderHandles = SelectMatchingExtensions(providerHandles, viewRoles); + return guardedOperations.InvokeBestMatchingFactory(roleMatchingProviderHandles, dataContentType, getter, contentTypeRegistryService, errorSource); + } + } +} diff --git a/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs b/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs new file mode 100644 index 0000000..34fe9f3 --- /dev/null +++ b/src/Text/Util/TextUIUtil/VacuousTextViewModel.cs @@ -0,0 +1,77 @@ +// +// 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.Utilities +{ + using System; + using Microsoft.VisualStudio.Utilities; + using Microsoft.VisualStudio.Text; + using Microsoft.VisualStudio.Text.Editor; + + /// <summary> + /// Implements a text view model that simply reexposes the data buffer and an optional edit buffer. The VisualBuffer + /// is the same as the edit buffer, which is in turn the same as the data buffer if no edit buffer is specified. + /// This is the default if no view model provider is specified or if the specified one declines to build a model. + /// </summary> + internal class VacuousTextViewModel : ITextViewModel + { + private ITextDataModel dataModel; + private ITextBuffer editBuffer; + + public VacuousTextViewModel(ITextDataModel dataModel) : this(dataModel, dataModel.DataBuffer) { } + + public VacuousTextViewModel(ITextDataModel dataModel, ITextBuffer editBuffer) + { + this.dataModel = dataModel; + this.editBuffer = editBuffer; + this.Properties = new PropertyCollection(); + } + + public ITextDataModel DataModel + { + get { return this.dataModel; } + } + + public ITextBuffer DataBuffer + { + get { return this.dataModel.DataBuffer; } + } + + public ITextBuffer EditBuffer + { + get { return this.editBuffer; } + } + + public ITextBuffer VisualBuffer + { + get { return this.EditBuffer; } + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public PropertyCollection Properties { get; internal set; } + + + public SnapshotPoint GetNearestPointInVisualBuffer(SnapshotPoint editBufferPoint) + { + // The edit buffer is the same as the visual buffer, so just return the passed-in point. + return editBufferPoint; + } + + public SnapshotPoint GetNearestPointInVisualSnapshot(SnapshotPoint editBufferPoint, ITextSnapshot targetVisualSnapshot, PointTrackingMode trackingMode) + { + // The edit buffer is the same as the visual buffer, so just return the passed-in point translated to the correct snapshot. + return editBufferPoint.TranslateTo(targetVisualSnapshot, trackingMode); + } + + public bool IsPointInVisualBuffer(SnapshotPoint editBufferPoint, PositionAffinity affinity) + { + // The edit buffer is the same as the visual buffer, so just return true. + return true; + } + } +} diff --git a/src/key.snk b/src/key.snk Binary files differnew file mode 100644 index 0000000..5ec4eb8 --- /dev/null +++ b/src/key.snk |