|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.IO;
using System.IO.Hashing;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServices;
using Roslyn.Utilities;
using Roslyn.VisualStudio.Next.UnitTests.UnifiedSettings.TestModel;
using Xunit;
namespace Roslyn.VisualStudio.Next.UnitTests.UnifiedSettings;
public class UnifiedSettingsTests
{
#region CSharpTest
/// <summary>
/// Dictionary containing the option to unified setting path for C#.
/// </summary>
private static readonly ImmutableDictionary<IOption2, string> s_csharpUnifiedSettingsStorage = ImmutableDictionary<IOption2, string>.Empty.
Add(CompletionOptionsStorage.TriggerOnTypingLetters, "textEditor.csharp.intellisense.triggerCompletionOnTypingLetters").
Add(CompletionOptionsStorage.TriggerOnDeletion, "textEditor.csharp.intellisense.triggerCompletionOnDeletion").
Add(CompletionOptionsStorage.TriggerInArgumentLists, "textEditor.csharp.intellisense.triggerCompletionInArgumentLists").
Add(CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems, "textEditor.csharp.intellisense.highlightMatchingPortionsOfCompletionListItems").
Add(CompletionViewOptionsStorage.ShowCompletionItemFilters, "textEditor.csharp.intellisense.showCompletionItemFilters").
Add(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon, "textEditor.csharp.intellisense.completeStatementOnSemicolon").
Add(CompletionOptionsStorage.SnippetsBehavior, "textEditor.csharp.intellisense.snippetsBehavior").
Add(CompletionOptionsStorage.EnterKeyBehavior, "textEditor.csharp.intellisense.returnKeyCompletionBehavior").
Add(CompletionOptionsStorage.ShowNameSuggestions, "textEditor.csharp.intellisense.showNameCompletionSuggestions").
Add(CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, "textEditor.csharp.intellisense.showCompletionItemsFromUnimportedNamespaces").
Add(CompletionViewOptionsStorage.EnableArgumentCompletionSnippets, "textEditor.csharp.intellisense.enableArgumentCompletionSnippets").
Add(CompletionOptionsStorage.ShowNewSnippetExperienceUserOption, "textEditor.csharp.intellisense.showNewSnippetExperience");
/// <summary>
/// Array containing the option to expected unified settings for C# intellisense page.
/// </summary>
private static readonly ImmutableArray<(IOption2, UnifiedSettingBase)> s_csharpIntellisenseExpectedSettings =
[
(CompletionOptionsStorage.TriggerOnTypingLetters, CreateBooleanOption(
CompletionOptionsStorage.TriggerOnTypingLetters,
title: "Show completion list after a character is typed",
order: 0,
languageName: LanguageNames.CSharp)),
(CompletionOptionsStorage.TriggerOnDeletion, CreateBooleanOption(
CompletionOptionsStorage.TriggerOnDeletion,
title: "Show completion list after a character is deleted",
order: 1,
customDefaultValue: false,
enableWhenOptionAndValue: (enableWhenOption: CompletionOptionsStorage.TriggerOnTypingLetters, whenValue: true),
languageName: LanguageNames.CSharp)),
(CompletionOptionsStorage.TriggerInArgumentLists, CreateBooleanOption(
CompletionOptionsStorage.TriggerInArgumentLists,
title: "Automatically show completion list in argument lists",
order: 10,
languageName: LanguageNames.CSharp)),
(CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems, CreateBooleanOption(
CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems,
"Highlight matching portions of completion list items",
order: 20,
languageName: LanguageNames.CSharp)),
(CompletionViewOptionsStorage.ShowCompletionItemFilters, CreateBooleanOption(
CompletionViewOptionsStorage.ShowCompletionItemFilters,
title: "Show completion item filters",
order: 30,
languageName: LanguageNames.CSharp)),
(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon, CreateBooleanOption(
CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon,
title: "Automatically complete statement on semicolon",
order: 40,
languageName: LanguageNames.CSharp)),
(CompletionOptionsStorage.SnippetsBehavior, CreateEnumOption(
CompletionOptionsStorage.SnippetsBehavior,
"Snippets behavior",
order: 50,
customDefaultValue: SnippetsRule.AlwaysInclude,
enumLabels: ["Never include snippets", "Always include snippets", "Include snippets when ?-Tab is typed after an identifier"],
enumValues: [SnippetsRule.NeverInclude, SnippetsRule.AlwaysInclude, SnippetsRule.IncludeAfterTypingIdentifierQuestionTab],
customMaps: [new Map { Result = "neverInclude", Match = 1 }, new Map { Result = "alwaysInclude", Match = 2 }, new Map { Result = "alwaysInclude", Match = 0 }, new Map { Result = "includeAfterTypingIdentifierQuestionTab", Match = 3 }],
languageName: LanguageNames.CSharp)),
(CompletionOptionsStorage.EnterKeyBehavior, CreateEnumOption(
CompletionOptionsStorage.EnterKeyBehavior,
"Enter key behavior",
order: 60,
customDefaultValue: EnterKeyRule.Never,
enumLabels: ["Never add new line on enter", "Only add new line on enter after end of fully typed word", "Always add new line on enter"],
enumValues: [EnterKeyRule.Never, EnterKeyRule.AfterFullyTypedWord, EnterKeyRule.Always],
customMaps: [new Map { Result = "never", Match = 1 }, new Map { Result = "never", Match = 0 }, new Map { Result = "always", Match = 2}, new Map { Result = "afterFullyTypedWord", Match = 3 }],
languageName: LanguageNames.CSharp)),
(CompletionOptionsStorage.ShowNameSuggestions, CreateBooleanOption(
CompletionOptionsStorage.ShowNameSuggestions,
title: "Show name suggestions",
order: 70,
languageName: LanguageNames.CSharp)),
(CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, CreateBooleanOption(
CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces,
title: "Show items from unimported namespaces",
order: 80,
languageName: LanguageNames.CSharp)),
(CompletionViewOptionsStorage.EnableArgumentCompletionSnippets, CreateBooleanOption(
CompletionViewOptionsStorage.EnableArgumentCompletionSnippets,
title: "Tab twice to insert arguments",
customDefaultValue: false,
order: 90,
languageName: LanguageNames.CSharp,
message: "Experimental feature")),
(CompletionOptionsStorage.ShowNewSnippetExperienceUserOption, CreateBooleanOption(
CompletionOptionsStorage.ShowNewSnippetExperienceUserOption,
title: "Show new snippet experience",
customDefaultValue: false,
order: 100,
languageName: LanguageNames.CSharp,
featureFlagAndExperimentValue: (CompletionOptionsStorage.ShowNewSnippetExperienceFeatureFlag, true),
message: "Experimental feature")),
];
[Fact]
public async Task CSharpCategoriesTest()
{
using var registrationFileStream = typeof(UnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("Roslyn.VisualStudio.Next.UnitTests.csharpSettings.registration.json");
var jsonDocument = await JsonNode.ParseAsync(registrationFileStream!, documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip });
var categories = jsonDocument!.Root["categories"]!.AsObject();
var propertyToCategory = categories.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Deserialize<Category>());
Assert.Equal(2, propertyToCategory.Count);
Assert.Equal("C#", propertyToCategory["textEditor.csharp"]!.Title);
Assert.Equal("IntelliSense", propertyToCategory["textEditor.csharp.intellisense"]!.Title);
Assert.Equal(Guids.CSharpOptionPageIntelliSenseIdString, propertyToCategory["textEditor.csharp.intellisense"]!.LegacyOptionPageId);
await VerifyTagAsync(jsonDocument.ToString(), "Roslyn.VisualStudio.Next.UnitTests.csharpPackageRegistration.pkgdef");
}
[Fact]
public async Task CSharpIntellisenseTest()
{
using var registrationFileStream = typeof(UnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("Roslyn.VisualStudio.Next.UnitTests.csharpSettings.registration.json");
var jsonDocument = await JsonNode.ParseAsync(registrationFileStream!, documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip });
foreach (var (option, _) in s_csharpIntellisenseExpectedSettings)
{
Assert.True(s_csharpUnifiedSettingsStorage.ContainsKey(option));
}
VerifyProperties(jsonDocument!, "textEditor.csharp.intellisense", s_csharpIntellisenseExpectedSettings);
await VerifyTagAsync(jsonDocument!.ToString(), "Roslyn.VisualStudio.Next.UnitTests.csharpPackageRegistration.pkgdef");
}
#endregion
#region VisualBasicTest
/// <summary>
/// Dictionary containing the option to unified setting path for VB.
/// </summary>
private static readonly ImmutableDictionary<IOption2, string> s_visualBasicUnifiedSettingsStorage = ImmutableDictionary<IOption2, string>.Empty.
Add(CompletionOptionsStorage.TriggerOnTypingLetters, "textEditor.basic.intellisense.triggerCompletionOnTypingLetters").
Add(CompletionOptionsStorage.TriggerOnDeletion, "textEditor.basic.intellisense.triggerCompletionOnDeletion").
Add(CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems, "textEditor.basic.intellisense.highlightMatchingPortionsOfCompletionListItems").
Add(CompletionViewOptionsStorage.ShowCompletionItemFilters, "textEditor.basic.intellisense.showCompletionItemFilters").
Add(CompletionOptionsStorage.SnippetsBehavior, "textEditor.basic.intellisense.snippetsBehavior").
Add(CompletionOptionsStorage.EnterKeyBehavior, "textEditor.basic.intellisense.returnKeyCompletionBehavior").
Add(CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, "textEditor.basic.intellisense.showCompletionItemsFromUnimportedNamespaces").
Add(CompletionViewOptionsStorage.EnableArgumentCompletionSnippets, "textEditor.basic.intellisense.enableArgumentCompletionSnippets");
/// <summary>
/// Array containing the option to expected unified settings for VB intellisense page.
/// </summary>
private static readonly ImmutableArray<(IOption2, UnifiedSettingBase)> s_visualBasicIntellisenseExpectedSettings =
[
(CompletionOptionsStorage.TriggerOnTypingLetters, CreateBooleanOption(
CompletionOptionsStorage.TriggerOnTypingLetters,
title: "Show completion list after a character is typed",
order: 0,
languageName: LanguageNames.VisualBasic)),
(CompletionOptionsStorage.TriggerOnDeletion, CreateBooleanOption(
CompletionOptionsStorage.TriggerOnDeletion,
title: "Show completion list after a character is deleted",
order: 1,
customDefaultValue: true,
languageName: LanguageNames.VisualBasic)),
(CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems, CreateBooleanOption(
CompletionViewOptionsStorage.HighlightMatchingPortionsOfCompletionListItems,
"Highlight matching portions of completion list items",
order: 10,
languageName: LanguageNames.VisualBasic)),
(CompletionViewOptionsStorage.ShowCompletionItemFilters, CreateBooleanOption(
CompletionViewOptionsStorage.ShowCompletionItemFilters,
title: "Show completion item filters",
order: 20,
languageName: LanguageNames.VisualBasic)),
(CompletionOptionsStorage.SnippetsBehavior, CreateEnumOption(
CompletionOptionsStorage.SnippetsBehavior,
"Snippets behavior",
order: 30,
customDefaultValue: SnippetsRule.IncludeAfterTypingIdentifierQuestionTab,
enumLabels: ["Never include snippets", "Always include snippets", "Include snippets when ?-Tab is typed after an identifier"],
enumValues: [SnippetsRule.NeverInclude, SnippetsRule.AlwaysInclude, SnippetsRule.IncludeAfterTypingIdentifierQuestionTab],
customMaps: [new Map { Result = "neverInclude", Match = 1 }, new Map { Result = "alwaysInclude", Match = 2 }, new Map { Result = "includeAfterTypingIdentifierQuestionTab", Match = 3 }, new Map { Result = "includeAfterTypingIdentifierQuestionTab", Match = 0 }],
languageName: LanguageNames.VisualBasic)),
(CompletionOptionsStorage.EnterKeyBehavior, CreateEnumOption(
CompletionOptionsStorage.EnterKeyBehavior,
"Enter key behavior",
order: 40,
customDefaultValue: EnterKeyRule.Always,
enumLabels: ["Never add new line on enter", "Only add new line on enter after end of fully typed word", "Always add new line on enter"],
enumValues: [EnterKeyRule.Never, EnterKeyRule.AfterFullyTypedWord, EnterKeyRule.Always],
customMaps: [new Map { Result = "never", Match = 1}, new Map { Result = "always", Match = 2}, new Map { Result = "always", Match = 0}, new Map { Result = "afterFullyTypedWord", Match = 3 }],
languageName: LanguageNames.VisualBasic)),
(CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces, CreateBooleanOption(
CompletionOptionsStorage.ShowItemsFromUnimportedNamespaces,
title: "Show items from unimported namespaces",
order: 50,
languageName: LanguageNames.VisualBasic)),
(CompletionViewOptionsStorage.EnableArgumentCompletionSnippets, CreateBooleanOption(
CompletionViewOptionsStorage.EnableArgumentCompletionSnippets,
title: "Tab twice to insert arguments",
customDefaultValue: false,
order: 60,
languageName: LanguageNames.VisualBasic,
message: "Experimental feature")),
];
[Fact]
public async Task VisualBasicCategoriesTest()
{
using var registrationFileStream = typeof(UnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("Roslyn.VisualStudio.Next.UnitTests.visualBasicSettings.registration.json");
var jsonDocument = await JsonNode.ParseAsync(registrationFileStream!, documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip });
var categories = jsonDocument!.Root["categories"]!.AsObject();
var propertyToCategory = categories.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Deserialize<Category>());
Assert.Equal(2, propertyToCategory.Count);
Assert.Equal("Visual Basic", propertyToCategory["textEditor.basic"]!.Title);
Assert.Equal("IntelliSense", propertyToCategory["textEditor.basic.intellisense"]!.Title);
Assert.Equal(Guids.VisualBasicOptionPageIntelliSenseIdString, propertyToCategory["textEditor.basic.intellisense"]!.LegacyOptionPageId);
await VerifyTagAsync(jsonDocument.ToString(), "Roslyn.VisualStudio.Next.UnitTests.visualBasicPackageRegistration.pkgdef");
}
[Fact]
public async Task VisualBasicIntellisenseTest()
{
using var registrationFileStream = typeof(UnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream("Roslyn.VisualStudio.Next.UnitTests.visualBasicSettings.registration.json");
var jsonDocument = await JsonNode.ParseAsync(registrationFileStream!, documentOptions: new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip });
foreach (var (option, _) in s_visualBasicIntellisenseExpectedSettings)
{
Assert.True(s_visualBasicUnifiedSettingsStorage.ContainsKey(option));
}
VerifyProperties(jsonDocument!, "textEditor.basic.intellisense", s_visualBasicIntellisenseExpectedSettings);
await VerifyTagAsync(jsonDocument!.ToString(), "Roslyn.VisualStudio.Next.UnitTests.visualBasicPackageRegistration.pkgdef");
}
private static void VerifyProperties(JsonNode jsonDocument, string prefix, ImmutableArray<(IOption2, UnifiedSettingBase)> expectedOptionToSettings)
{
var properties = jsonDocument!.Root["properties"]!.AsObject()
.Where(jsonObject => jsonObject.Key.StartsWith(prefix))
.SelectAsArray(jsonObject => jsonObject.Value);
Assert.Equal(expectedOptionToSettings.Length, properties.Length);
foreach (var (actualJson, (expectedOption, expectedSetting)) in properties.Zip(expectedOptionToSettings, (actual, expected) => (actual, expected)))
{
// We only have bool and enum option now.
UnifiedSettingBase actualSetting = expectedOption.Definition.Type.IsEnum
? actualJson.Deserialize<UnifiedSettingsEnumOption>()!
: actualJson.Deserialize<UnifiedSettingsOption<bool>>()!;
Assert.Equal(expectedSetting, actualSetting);
}
}
#endregion
#region Helpers
private static async Task VerifyTagAsync(string registrationFile, string pkgdefFileName)
{
using var pkgDefFileStream = typeof(UnifiedSettingsTests).GetTypeInfo().Assembly.GetManifestResourceStream(pkgdefFileName);
using var streamReader = new StreamReader(pkgDefFileStream!);
var pkgdefFile = await streamReader.ReadToEndAsync();
var fileBytes = Encoding.ASCII.GetBytes(registrationFile);
var expectedTags = BitConverter.ToInt64([.. XxHash128.Hash(fileBytes).Take(8)], 0).ToString("X16");
var regex = new Regex("""
"CacheTag"=qword:\w{16}
""");
var match = regex.Match(pkgdefFile, 0).Value;
var actualTag = match[^16..];
// Please change the CacheTag value in pkddefFile when you modify the registration file.
Assert.Equal(expectedTags, actualTag);
}
private static UnifiedSettingsOption<bool> CreateBooleanOption(
IOption2 onboardedOption,
string title,
int order,
string languageName,
bool? customDefaultValue = null,
(IOption2 featureFlagOption, bool value)? featureFlagAndExperimentValue = null,
(IOption2 enableWhenOption, object whenValue)? enableWhenOptionAndValue = null,
string? message = null)
{
var migration = new Migration { Pass = new Pass { Input = new Input(onboardedOption, languageName) } };
var type = onboardedOption.Definition.Type;
// If the option's type is nullable type, like bool?, we use bool in the registration file.
var underlyingType = Nullable.GetUnderlyingType(type);
var nonNullableType = underlyingType ?? type;
var alternativeDefault = featureFlagAndExperimentValue is not null
? new AlternativeDefault<bool>(featureFlagAndExperimentValue.Value.featureFlagOption, featureFlagAndExperimentValue.Value.value)
: null;
var enableWhen = enableWhenOptionAndValue is not null
? $"${{config:{GetUnifiedSettingsOptionValue(enableWhenOptionAndValue.Value.enableWhenOption, languageName)}}}=='{enableWhenOptionAndValue.Value.whenValue.ToString().ToCamelCase()}'"
: null;
var expectedDefault = customDefaultValue ?? onboardedOption.Definition.DefaultValue;
// If the option default value is null, it means the option is in experiment mode and is hidden by a feature flag.
// In Unified Settings it is not allowed and should be replaced by using the alternative default.
// Like:
// "textEditor.csharp.intellisense.showNewSnippetExperience": {
// "type": "boolean",
// "default": false,
// "alternateDefault": {
// "flagName": "Roslyn.SnippetCompletion",
// "default": true
// }
// }
// so please specify a non-null default value.
Assert.NotNull(expectedDefault);
return new UnifiedSettingsOption<bool>
{
Title = title,
Type = nonNullableType.Name.ToCamelCase(),
Order = order,
EnableWhen = enableWhen,
Migration = migration,
AlternateDefault = alternativeDefault,
Default = (bool)expectedDefault,
Messages = message is null ? null : [new Message { Text = message }],
};
}
private static UnifiedSettingsEnumOption CreateEnumOption<T>(
IOption2 onboardedOption,
string title,
int order,
string[] enumLabels,
string languageName,
T? customDefaultValue = default,
T[]? enumValues = null,
Map[]? customMaps = null,
(IOption2 featureFlagOption, T value)? featureFlagAndExperimentValue = null,
(IOption2 enableWhenOption, object whenValue)? enableWhenOptionAndValue = null) where T : Enum
{
var type = onboardedOption.Definition.Type;
// If the option's type is nullable type, we use the original type in the registration file.
var nonNullableType = Nullable.GetUnderlyingType(type) ?? type;
Assert.Equal(typeof(T), nonNullableType);
var expectedEnumValues = enumValues ?? [.. Enum.GetValues(nonNullableType).Cast<T>()];
var migration = new Migration
{
EnumIntegerToString = new EnumIntegerToString
{
Input = new Input(onboardedOption, languageName),
Map = customMaps ?? [.. expectedEnumValues.Select(value => new Map { Result = value.ToString().ToCamelCase(), Match = Convert.ToInt32(value) })]
}
};
var alternativeDefault = featureFlagAndExperimentValue is not null
? new AlternativeDefault<string>(featureFlagAndExperimentValue.Value.featureFlagOption, featureFlagAndExperimentValue.Value.value.ToString().ToCamelCase())
: null;
var enableWhen = enableWhenOptionAndValue is not null
? $"${{config:{GetUnifiedSettingsOptionValue(enableWhenOptionAndValue.Value.enableWhenOption, languageName)}}}=='{enableWhenOptionAndValue.Value.whenValue.ToString().ToCamelCase()}'"
: null;
var expectedDefault = customDefaultValue ?? onboardedOption.Definition.DefaultValue;
Assert.NotNull(expectedDefault);
return new UnifiedSettingsEnumOption
{
Title = title,
Type = "string",
Enum = [.. expectedEnumValues.Select(value => value.ToString().ToCamelCase())],
EnumItemLabels = enumLabels,
Order = order,
EnableWhen = enableWhen,
Migration = migration,
AlternateDefault = alternativeDefault,
Default = expectedDefault.ToString().ToCamelCase(),
};
}
private static string GetUnifiedSettingsOptionValue(IOption2 option, string languageName)
{
return languageName switch
{
LanguageNames.CSharp => s_csharpUnifiedSettingsStorage[option],
LanguageNames.VisualBasic => s_visualBasicUnifiedSettingsStorage[option],
_ => throw ExceptionUtilities.UnexpectedValue(languageName)
};
}
#endregion
}
|