diff --git a/src/EditorFeatures/Core.Wpf/Options/LegacyEditorConfigDocumentOptionsProvider.DocumentOptions.cs b/src/EditorFeatures/Core.Wpf/Options/LegacyEditorConfigDocumentOptionsProvider.DocumentOptions.cs index f82a35b50816653aced5223fec3b33356a4938e4..be428d4e67019b0924038045607aa1bdd439cb9f 100644 --- a/src/EditorFeatures/Core.Wpf/Options/LegacyEditorConfigDocumentOptionsProvider.DocumentOptions.cs +++ b/src/EditorFeatures/Core.Wpf/Options/LegacyEditorConfigDocumentOptionsProvider.DocumentOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -40,10 +41,10 @@ public bool TryGetDocumentOption(OptionKey option, OptionSet underlyingOptions, // Dictionary from the old system. // // We cache this with a conditional weak table so we're able to maintain the assumptions in EditorConfigNamingStyleParser - // that the instance doesn't regularly change and thus can be used for further caching + // that the instance doesn't regularly change and thus can be used for further caching. var allRawConventions = s_convertedDictionaryCache.GetValue( _codingConventionSnapshot.AllRawConventions, - d => ImmutableDictionary.CreateRange(d.Select(c => KeyValuePairUtil.Create(c.Key, c.Value.ToString())))); + d => new StringConvertingDictionary(d)); try { @@ -57,6 +58,57 @@ public bool TryGetDocumentOption(OptionKey option, OptionSet underlyingOptions, return false; } } + + /// + /// A class that implements atop a + /// where we just convert the values to strings with ToString(). Ordering of the underlying dictionary is preserved, so that way + /// code that relies on the underlying ordering of the underlying dictionary isn't affected. + /// + private class StringConvertingDictionary : IReadOnlyDictionary + { + private readonly IReadOnlyDictionary _underlyingDictionary; + + public StringConvertingDictionary(IReadOnlyDictionary underlyingDictionary) + { + _underlyingDictionary = underlyingDictionary ?? throw new ArgumentNullException(nameof(underlyingDictionary)); + } + + public string this[string key] => _underlyingDictionary[key]?.ToString(); + + public IEnumerable Keys => _underlyingDictionary.Keys; + public IEnumerable Values => _underlyingDictionary.Values.Select(s => s?.ToString()); + + public int Count => _underlyingDictionary.Count; + + public bool ContainsKey(string key) => _underlyingDictionary.ContainsKey(key); + + public IEnumerator> GetEnumerator() + { + foreach (var pair in _underlyingDictionary) + { + yield return new KeyValuePair(pair.Key, pair.Value?.ToString()); + } + } + + public bool TryGetValue(string key, out string value) + { + if (_underlyingDictionary.TryGetValue(key, out object objectValue)) + { + value = objectValue?.ToString(); + return true; + } + else + { + value = null; + return false; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } } } } diff --git a/src/EditorFeatures/Test/EditorConfig/LegacyEditorConfigDocumentOptionsProviderTests.cs b/src/EditorFeatures/Test/EditorConfig/LegacyEditorConfigDocumentOptionsProviderTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..b04431771f7fd76fcd3ed73093b3ccbb9f672dfd --- /dev/null +++ b/src/EditorFeatures/Test/EditorConfig/LegacyEditorConfigDocumentOptionsProviderTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Composition; +using System.IO; +using System.Linq; +using System.Threading; +using System.Xml.Linq; +using Microsoft.CodeAnalysis.Editor.Options; +using Microsoft.CodeAnalysis.Editor.UnitTests; +using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.VisualStudio.CodingConventions; +using Microsoft.VisualStudio.Composition; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests.EditorConfigStorageLocation +{ + [UseExportProvider] + public class LegacyEditorConfigDocumentOptionsProviderTests + { + [Fact] + public void OrderingOfEditorConfigMaintained() + { + using var tempRoot = new TempRoot(); + var tempDirectory = tempRoot.CreateDirectory(); + + // Write out an .editorconfig. We'll write out 100 random GUIDs + var expectedKeysInOrder = new List(); + + using (var writer = new StreamWriter(tempDirectory.CreateFile(".editorconfig").Path)) + { + writer.WriteLine("root = true"); + writer.WriteLine("[*.cs]"); + + for (int i = 0; i < 100; i++) + { + var key = Guid.NewGuid().ToString(); + expectedKeysInOrder.Add(key); + writer.WriteLine($"{key} = value"); + } + } + + // Create a workspace with a file in that path + var codingConventionsCatalog = ExportProviderCache.GetOrCreateAssemblyCatalog(typeof(ICodingConventionsManager).Assembly).WithPart(typeof(MockFileWatcher)); + var exportProvider = ExportProviderCache.GetOrCreateExportProviderFactory(TestExportProvider.EntireAssemblyCatalogWithCSharpAndVisualBasic.WithParts(codingConventionsCatalog)).CreateExportProvider(); + + using var workspace = TestWorkspace.CreateWorkspace( + new XElement("Workspace", + new XElement("Project", new XAttribute("Language", "C#"), + new XElement("Document", new XAttribute("FilePath", tempDirectory.CreateFile("Test.cs").Path)))), exportProvider: exportProvider); + + var document = workspace.CurrentSolution.Projects.Single().Documents.Single(); + + var providerFactory = workspace.ExportProvider.GetExportedValues().OfType().Single(); + var provider = providerFactory.TryCreate(workspace); + + var option = new Option>(nameof(LegacyEditorConfigDocumentOptionsProviderTests), nameof(OrderingOfEditorConfigMaintained), null, new[] { new KeysReturningStorageLocation() }); + var optionKey = new OptionKey(option); + + // Fetch the underlying option order with a "option" that returns the keys + provider.GetOptionsForDocumentAsync(document, CancellationToken.None).Result.TryGetDocumentOption(optionKey, workspace.Options, out object actualKeysInOrderObject); + + var actualKeysInOrder = Assert.IsAssignableFrom>(actualKeysInOrderObject); + + Assert.Equal(expectedKeysInOrder, actualKeysInOrder); + } + + [PartNotDiscoverable] + [Export(typeof(IFileWatcher))] + [Shared] + private class MockFileWatcher : IFileWatcher + { +#pragma warning disable CS0067 // the event is unused + + public event ConventionsFileChangedAsyncEventHandler ConventionFileChanged; + public event ContextFileMovedAsyncEventHandler ContextFileMoved; + +#pragma warning restore CS0067 + + public void Dispose() + { + } + + public void StartWatching(string fileName, string directoryPath) + { + } + + public void StopWatching(string fileName, string directoryPath) + { + } + } + + /// + /// An option storage location that returns as the value all the keys in the order they came from the underlying storage. + /// + private class KeysReturningStorageLocation : OptionStorageLocation, IEditorConfigStorageLocation + { + public bool TryGetOption(object underlyingOption, IReadOnlyDictionary rawOptions, Type type, out object value) + { + value = rawOptions.Keys; + + return true; + } + } + } +} diff --git a/src/EditorFeatures/Test/Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj b/src/EditorFeatures/Test/Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj index a5b6b578d5d2608d818d1af5bcf27b3f86eede54..07b5ee8fa418efacd2d415e8fa76f6eeaeaa698d 100644 --- a/src/EditorFeatures/Test/Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj +++ b/src/EditorFeatures/Test/Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj @@ -55,6 +55,7 @@ + diff --git a/src/Workspaces/CoreTest/EditorConfigStorageLocation/EditorConfigStorageLocationTests.cs b/src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs similarity index 98% rename from src/Workspaces/CoreTest/EditorConfigStorageLocation/EditorConfigStorageLocationTests.cs rename to src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs index eccc5c61ae49297b70af8ff0d7219eeb86327a61..c008a9241ab2aeaa287b87f9ec7f1e43217443b2 100644 --- a/src/Workspaces/CoreTest/EditorConfigStorageLocation/EditorConfigStorageLocationTests.cs +++ b/src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.UnitTests.EditorConfig.StorageLocation { - public class EditorConfigStorageLocationTests + public class NamingStylePreferenceEditorConfigStorageLocationTests { [Fact] public static void TestEmptyDictionaryReturnNoNamingStylePreferencesObjectReturnsFalse()