Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion Terminal.Gui/Configuration/ConfigurationManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ internal static void Initialize ()
// Some getters (notably ThemeManager.Themes while uninitialized) synthesize composite
// objects from other configuration properties, so a single populate-and-clone pass can
// observe half-initialized cache entries and persist null/default values into the clone.
hardCodedProperty.Value.PropertyValue = DeepCloner.DeepClone (hardCodedProperty.Value.PropertyValue);
hardCodedProperty.Value.PropertyValue = CloneHardCodedPropertyValue (hardCodedProperty.Value.PropertyValue);
hardCodedProperty.Value.Immutable = true;
}
}
Expand All @@ -222,6 +222,58 @@ internal static void Initialize ()
ThemeManager.Themes? [ThemeManager.Theme].Apply ();
}

// AOT trimming removes the reflective Dictionary<,> constructor that DeepCloner relies on.
// Each non-trivially-cloneable config property type needs a typed clone path here to stay AOT-safe.
private static object? CloneHardCodedPropertyValue (object? propertyValue)
{
if (propertyValue is Dictionary<Command, PlatformKeyBinding> keyBindings)
{
return CloneKeyBindings (keyBindings);
}

return DeepCloner.DeepClone (propertyValue);
}

private static Dictionary<Command, PlatformKeyBinding> CloneKeyBindings (Dictionary<Command, PlatformKeyBinding> source)
{
Dictionary<Command, PlatformKeyBinding> clone = new (source.Comparer);

foreach (KeyValuePair<Command, PlatformKeyBinding> kvp in source)
{
clone [kvp.Key] = ClonePlatformKeyBinding (kvp.Value);
}

return clone;
}

private static PlatformKeyBinding ClonePlatformKeyBinding (PlatformKeyBinding binding)
{
return binding with
{
All = CloneKeyArray (binding.All),
Windows = CloneKeyArray (binding.Windows),
Linux = CloneKeyArray (binding.Linux),
Macos = CloneKeyArray (binding.Macos)
};
}
Comment thread
tig marked this conversation as resolved.

private static Key []? CloneKeyArray (Key []? keys)
{
if (keys is null)
{
return null;
}

Key [] clonedKeys = new Key [keys.Length];

for (var i = 0; i < keys.Length; i++)
{
clonedKeys [i] = new (keys [i]);
}

return clonedKeys;
}

#endregion Initialization

#region Enable/Disable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,73 @@ public void HardCodedDefaultCache_Properties_Are_Copies ()
Disable (true);
}

[Fact]
public void HardCodedDefaultCache_KeyBindingDictionaries_Are_Typed_Deep_Copies ()
{
// Copilot
Assert.False (IsEnabled);
Application.ResetState (true);

try
{
AssertKeyBindingDictionaryIsDeepCopy ("Application.DefaultKeyBindings", Application.DefaultKeyBindings!);
AssertKeyBindingDictionaryIsDeepCopy ("View.DefaultKeyBindings", View.DefaultKeyBindings!);
}
finally
{
Disable (true);
Application.ResetState (true);
}

static void AssertKeyBindingDictionaryIsDeepCopy (string propertyName, Dictionary<Command, PlatformKeyBinding> currentBindings)
{
FrozenDictionary<string, ConfigProperty> cache = GetHardCodedConfigPropertyCache ();
Dictionary<Command, PlatformKeyBinding> cachedBindings = Assert.IsType<Dictionary<Command, PlatformKeyBinding>> (cache [propertyName].PropertyValue);

Assert.NotSame (currentBindings, cachedBindings);
Assert.NotEmpty (cachedBindings);

KeyValuePair<Command, PlatformKeyBinding> cachedEntry = cachedBindings.First (kvp => HasAnyKeys (kvp.Value));
PlatformKeyBinding currentBinding = currentBindings [cachedEntry.Key];

Assert.NotSame (currentBinding, cachedEntry.Value);
AssertKeyArraysAreDeepCopies (currentBinding.All, cachedEntry.Value.All);
AssertKeyArraysAreDeepCopies (currentBinding.Windows, cachedEntry.Value.Windows);
AssertKeyArraysAreDeepCopies (currentBinding.Linux, cachedEntry.Value.Linux);
AssertKeyArraysAreDeepCopies (currentBinding.Macos, cachedEntry.Value.Macos);
}

static bool HasAnyKeys (PlatformKeyBinding binding)
{
return binding.All is { Length: > 0 }
|| binding.Windows is { Length: > 0 }
|| binding.Linux is { Length: > 0 }
|| binding.Macos is { Length: > 0 };
}

#pragma warning disable CS8632 // Nullable annotations document the nullable PlatformKeyBinding properties in this non-nullable test file.
static void AssertKeyArraysAreDeepCopies (Key []? currentKeys, Key []? cachedKeys)
#pragma warning restore CS8632
{
if (currentKeys is null)
{
Assert.Null (cachedKeys);

return;
}

Assert.NotNull (cachedKeys);
Assert.NotSame (currentKeys, cachedKeys);
Assert.Equal (currentKeys.Length, cachedKeys.Length);

for (var i = 0; i < currentKeys.Length; i++)
{
Assert.NotSame (currentKeys [i], cachedKeys [i]);
Assert.Equal (currentKeys [i], cachedKeys [i]);
}
}
}

[Fact]
public void HardCoded_Default_Theme_Uses_Fully_Populated_Cache_Values ()
{
Expand Down
Loading