diff --git a/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/FluentLauncher.Infra.LocalizedStrings.SourceGenerators.csproj b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/FluentLauncher.Infra.LocalizedStrings.SourceGenerators.csproj new file mode 100644 index 0000000..ca55a47 --- /dev/null +++ b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/FluentLauncher.Infra.LocalizedStrings.SourceGenerators.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + true + 13.0 + enable + true + + + + + AnyCPU + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/LocalizedStringsGenerator.cs b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/LocalizedStringsGenerator.cs new file mode 100644 index 0000000..37fa4b7 --- /dev/null +++ b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/LocalizedStringsGenerator.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Resources; +using System.Text; +using System.Xml.Linq; +using CommunityToolkit.Mvvm.SourceGenerators.Extensions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace FluentLauncher.Infra.Settings.SourceGenerators; + +internal record struct ClassInfo(string Namespace, string ClassName); + +internal record struct ReswFileInfo(string FilePath) +{ + public string Filename => Path.GetFileNameWithoutExtension(FilePath); + public string ResourceMapName => Filename.Split('.')[0]; + public string Qualifier => Filename.Substring(ResourceMapName.Length, Filename.Length - ResourceMapName.Length - ".resw".Length); +} + +[Generator(LanguageNames.CSharp)] +public class LocalizedStringsGenerator : IIncrementalGenerator +{ + public LocalizedStringsGenerator() + { +#if DEBUG + if (!Debugger.IsAttached) + { + //Debugger.Launch(); + } +#endif + } + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Find all classes with the [GeneratedLocalizedStrings] attribute + var classDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName( + "FluentLauncher.Infra.LocalizedStrings.GeneratedLocalizedStringsAttribute", + static (node, _) => node is ClassDeclarationSyntax, + static (ctx, token) => + { + // Extract class info + ITypeSymbol localizedStringClassSymbol = (ITypeSymbol)ctx.TargetSymbol; + string containingNamespace = localizedStringClassSymbol.ContainingNamespace.ToDisplayString(); + string className = localizedStringClassSymbol.Name; + return new ClassInfo(containingNamespace, className); + }) + .Collect(); + + // Find all .resw files, group by resource map name and keep the neutral one (or the first in alphabetical order) + var reswFilesProvider = context.AdditionalTextsProvider + // Find all .resw files + .Where(file => file.Path.EndsWith(".resw", StringComparison.OrdinalIgnoreCase)) + .Select((file, token) => + { + var reswFile = new ReswFileInfo(file.Path); + return (reswFile.ResourceMapName, reswFile); + }) + .GroupBy( + static item => item.Left, + static item => item.Right + ) + .Select((group, token) => group.Right + .ToList() + .OrderBy(file => file.Filename) + .First() + ) + .Collect(); + + context.RegisterSourceOutput(classDeclarations.Combine(reswFilesProvider), Execute); + } + + private static void Execute(SourceProductionContext context, (ImmutableArray classInfos, ImmutableArray reswFiles) input) + { + var (classes, reswFiles) = input; + if (classes.IsDefaultOrEmpty || reswFiles.IsDefaultOrEmpty) + return; + + foreach (var classInfo in classes) + { + var namespaceName = classInfo.Namespace; + var className = classInfo.ClassName; + + // Parse and generate properties for each .resw file + IEnumerable defaultStringIds = []; // Strings in Resources.resw + var otherStringIds = new Dictionary>(); + + foreach (var reswFile in reswFiles) + { + string resourceMapName = reswFile.ResourceMapName; + if (reswFile.Filename.Equals("Resources", StringComparison.OrdinalIgnoreCase)) + defaultStringIds = ParseReswFile(reswFile); + else + otherStringIds[resourceMapName] = ParseReswFile(reswFile); + } + + // Generate the class in the detected namespace + string source = GenerateClass(namespaceName, className, defaultStringIds, otherStringIds); + + // Add the generated source to the compilation + context.AddSource($"{namespaceName}.{className}.g.cs", SourceText.From(source, Encoding.UTF8)); + } + } + + private static IEnumerable ParseReswFile(ReswFileInfo reswFile) + { + using var reader = new StreamReader(reswFile.FilePath); + + IEnumerable? stringIds = System.Xml.Linq.XDocument.Load(reader).Root? + .Elements("data") + .Select(node => node.Attribute("name")?.Value.Replace(".", "/")) + .Where(name => !string.IsNullOrWhiteSpace(name))!; + + return stringIds ?? []; + //properties.Add($"public static string {propertyName} => s_resourceMap.GetValue(\"{namespaceName}/{name}\").ValueAsString;"); + } + + private static string GenerateClass( + string namespaceName, + string className, + IEnumerable defaultStringIds, + Dictionary> otherStringIds) + { + var propertyBuilder = new StringBuilder(); + + propertyBuilder.AppendLine("// Default resource map (Resources.resw)"); + foreach (var id in defaultStringIds) + { + string propertyName = id.Replace('/', '_').Replace(' ', '_'); + propertyBuilder.AppendLine($" public static string {propertyName} => s_resourceMap.GetValue(\"/Resources/{id}\").ValueAsString;"); + } + + propertyBuilder.AppendLine("\n // Other resource maps"); + foreach (var item in otherStringIds) + { + string resourceMapName = item.Key; + IEnumerable stringIds = item.Value; + propertyBuilder.AppendLine($" public static class {resourceMapName}") + .AppendLine(" {"); + + foreach (string id in stringIds) + { + string propertyName = id.Replace('/', '_').Replace(' ', '_'); + propertyBuilder.AppendLine($" public static string {propertyName} => s_resourceMap.GetValue(\"/{resourceMapName}/{id}\").ValueAsString;"); + } + + propertyBuilder.AppendLine(" }"); + } + + return $$""" + using global::Microsoft.Windows.ApplicationModel.Resources; + + namespace {{namespaceName}} + { + static partial class {{className}} + { + private static ResourceManager s_resourceManager; + private static ResourceMap s_resourceMap; + + static {{className}}() + { + s_resourceManager = new ResourceManager(); + s_resourceMap = s_resourceManager.MainResourceMap; + } + + {{propertyBuilder}} + } + } + """; + } +} diff --git a/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/EquatableArray{T}.cs b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/EquatableArray{T}.cs new file mode 100644 index 0000000..109e683 --- /dev/null +++ b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/EquatableArray{T}.cs @@ -0,0 +1,202 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Helpers; + +/// +/// Extensions for . +/// +internal static class EquatableArray +{ + /// + /// Creates an instance from a given . + /// + /// The type of items in the input array. + /// The input instance. + /// An instance from a given . + public static EquatableArray AsEquatableArray(this ImmutableArray array) + where T : IEquatable + { + return new(array); + } +} + +/// +/// An immutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +internal readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + /// + /// The underlying array. + /// + private readonly T[]? array; + + /// + /// Creates a new instance. + /// + /// The input to wrap. + public EquatableArray(ImmutableArray array) + { + this.array = Unsafe.As, T[]?>(ref array); + } + + /// + /// Gets a reference to an item at a specified position within the array. + /// + /// The index of the item to retrieve a reference to. + /// A reference to an item at a specified position within the array. + public ref readonly T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => ref AsImmutableArray().ItemRef(index); + } + + /// + /// Gets a value indicating whether the current array is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsEmpty; + } + + /// + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals(/*[NotNullWhen(true)]*/ object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override int GetHashCode() + { + if (this.array is not T[] array) + { + return 0; + } + + HashCode hashCode = default; + + foreach (T item in array) + { + hashCode.Add(item); + } + + return hashCode.ToHashCode(); + } + + /// + /// Gets an instance from the current . + /// + /// The from the current . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray AsImmutableArray() + { + return Unsafe.As>(ref Unsafe.AsRef(in this.array)); + } + + /// + /// Creates an instance from a given . + /// + /// The input instance. + /// An instance from a given . + public static EquatableArray FromImmutableArray(ImmutableArray array) + { + return new(array); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return AsImmutableArray().AsSpan(); + } + + /// + /// Copies the contents of this instance to a mutable array. + /// + /// The newly instantiated array. + public T[] ToArray() + { + return AsImmutableArray().ToArray(); + } + + /// + /// Gets an value to traverse items in the current array. + /// + /// An value to traverse items in the current array. + public ImmutableArray.Enumerator GetEnumerator() + { + return AsImmutableArray().GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator EquatableArray(ImmutableArray array) + { + return FromImmutableArray(array); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator ImmutableArray(EquatableArray array) + { + return array.AsImmutableArray(); + } + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} \ No newline at end of file diff --git a/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/HashCode.cs b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/HashCode.cs new file mode 100644 index 0000000..2d275af --- /dev/null +++ b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/HashCode.cs @@ -0,0 +1,188 @@ +// 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.ComponentModel; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +#pragma warning disable CS0809 + +namespace System; + +/// +/// A polyfill type that mirrors some methods from on .NET 6. +/// +internal struct HashCode +{ + private const uint Prime1 = 2654435761U; + private const uint Prime2 = 2246822519U; + private const uint Prime3 = 3266489917U; + private const uint Prime4 = 668265263U; + private const uint Prime5 = 374761393U; + + private static readonly uint seed = GenerateGlobalSeed(); + + private uint v1, v2, v3, v4; + private uint queue1, queue2, queue3; + private uint length; + + /// + /// Initializes the default seed. + /// + /// A random seed. + private static unsafe uint GenerateGlobalSeed() + { + byte[] bytes = new byte[4]; + + using (RandomNumberGenerator generator = RandomNumberGenerator.Create()) + { + generator.GetBytes(bytes); + } + + return BitConverter.ToUInt32(bytes, 0); + } + + /// + /// Adds a single value to the current hash. + /// + /// The type of the value to add into the hash code. + /// The value to add into the hash code. + public void Add(T value) + { + Add(value?.GetHashCode() ?? 0); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4) + { + v1 = seed + Prime1 + Prime2; + v2 = seed + Prime2; + v3 = seed; + v4 = seed - Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Round(uint hash, uint input) + { + return RotateLeft(hash + input * Prime2, 13) * Prime1; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint QueueRound(uint hash, uint queuedValue) + { + return RotateLeft(hash + queuedValue * Prime3, 17) * Prime4; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixState(uint v1, uint v2, uint v3, uint v4) + { + return RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixEmptyState() + { + return seed + Prime5; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint MixFinal(uint hash) + { + hash ^= hash >> 15; + hash *= Prime2; + hash ^= hash >> 13; + hash *= Prime3; + hash ^= hash >> 16; + + return hash; + } + + private void Add(int value) + { + uint val = (uint)value; + uint previousLength = this.length++; + uint position = previousLength % 4; + + if (position == 0) + { + this.queue1 = val; + } + else if (position == 1) + { + this.queue2 = val; + } + else if (position == 2) + { + this.queue3 = val; + } + else + { + if (previousLength == 3) + { + Initialize(out this.v1, out this.v2, out this.v3, out this.v4); + } + + this.v1 = Round(this.v1, this.queue1); + this.v2 = Round(this.v2, this.queue2); + this.v3 = Round(this.v3, this.queue3); + this.v4 = Round(this.v4, val); + } + } + + /// + /// Gets the resulting hashcode from the current instance. + /// + /// The resulting hashcode from the current instance. + public int ToHashCode() + { + uint length = this.length; + uint position = length % 4; + uint hash = length < 4 ? MixEmptyState() : MixState(this.v1, this.v2, this.v3, this.v4); + + hash += length * 4; + + if (position > 0) + { + hash = QueueRound(hash, this.queue1); + + if (position > 1) + { + hash = QueueRound(hash, this.queue2); + + if (position > 2) + { + hash = QueueRound(hash, this.queue3); + } + } + } + + hash = MixFinal(hash); + + return (int)hash; + } + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes. Use ToHashCode to retrieve the computed hash code.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => throw new NotSupportedException(); + + /// + [Obsolete("HashCode is a mutable struct and should not be compared with other HashCodes.", error: true)] + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => throw new NotSupportedException(); + + /// + /// Rotates the specified value left by the specified number of bits. + /// Similar in behavior to the x86 instruction ROL. + /// + /// The value to rotate. + /// The number of bits to rotate by. + /// Any value outside the range [0..31] is treated as congruent mod 32. + /// The rotated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RotateLeft(uint value, int offset) + { + return (value << offset) | (value >> (32 - offset)); + } +} diff --git a/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/IncrementalValuesProviderExtensions.cs b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/IncrementalValuesProviderExtensions.cs new file mode 100644 index 0000000..67f39ed --- /dev/null +++ b/FluentLauncher.Infra.LocalizedStrings.SourceGenerators/MVVMTKHelpers/IncrementalValuesProviderExtensions.cs @@ -0,0 +1,73 @@ +// 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. + +// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp), +// more info in ThirdPartyNotices.txt in the root of the project. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using CommunityToolkit.Mvvm.SourceGenerators.Helpers; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.Mvvm.SourceGenerators.Extensions; + +/// +/// Extension methods for . +/// +internal static class IncrementalValuesProviderExtensions +{ + /// + /// Groups items in a given sequence by a specified key. + /// + /// The type of left items in each tuple. + /// The type of right items in each tuple. + /// The type of resulting key elements. + /// The type of resulting projected elements. + /// The input instance. + /// The key selection . + /// The element selection . + /// An with the grouped results. + public static IncrementalValuesProvider<(TKey Key, EquatableArray Right)> GroupBy( + this IncrementalValuesProvider<(TLeft Left, TRight Right)> source, + Func<(TLeft Left, TRight Right), TKey> keySelector, + Func<(TLeft Left, TRight Right), TElement> elementSelector) + where TLeft : IEquatable + where TRight : IEquatable + where TKey : IEquatable + where TElement : IEquatable + { + return source.Collect().SelectMany((item, token) => + { + Dictionary.Builder> map = new(); + + foreach ((TLeft, TRight) pair in item) + { + TKey key = keySelector(pair); + TElement element = elementSelector(pair); + + if (!map.TryGetValue(key, out ImmutableArray.Builder builder)) + { + builder = ImmutableArray.CreateBuilder(); + + map.Add(key, builder); + } + + builder.Add(element); + } + + token.ThrowIfCancellationRequested(); + + ImmutableArray<(TKey Key, EquatableArray Elements)>.Builder result = + ImmutableArray.CreateBuilder<(TKey, EquatableArray)>(); + + foreach (KeyValuePair.Builder> entry in map) + { + result.Add((entry.Key, entry.Value.ToImmutable())); + } + + return result; + }); + } +} diff --git a/FluentLauncher.Infra.LocalizedStrings/FluentLauncher.Infra.LocalizedStrings.csproj b/FluentLauncher.Infra.LocalizedStrings/FluentLauncher.Infra.LocalizedStrings.csproj new file mode 100644 index 0000000..0672ddb --- /dev/null +++ b/FluentLauncher.Infra.LocalizedStrings/FluentLauncher.Infra.LocalizedStrings.csproj @@ -0,0 +1,8 @@ + + + + net8.0 + enable + + + diff --git a/FluentLauncher.Infra.LocalizedStrings/GeneratedLocalizedStringsAttribute.cs b/FluentLauncher.Infra.LocalizedStrings/GeneratedLocalizedStringsAttribute.cs new file mode 100644 index 0000000..37af6a9 --- /dev/null +++ b/FluentLauncher.Infra.LocalizedStrings/GeneratedLocalizedStringsAttribute.cs @@ -0,0 +1,6 @@ +using System; + +namespace FluentLauncher.Infra.LocalizedStrings; + +[AttributeUsage(AttributeTargets.Class)] +public class GeneratedLocalizedStringsAttribute : Attribute { }