// 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.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.Win32;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Options
{
///
/// Serializes options marked with to the local hive-specific registry.
///
[Export(typeof(IOptionPersister))]
internal sealed class LocalUserRegistryOptionPersister : IOptionPersister
{
///
/// An object to gate access to .
///
private readonly object _gate = new();
private readonly RegistryKey _registryKey;
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public LocalUserRegistryOptionPersister([Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider)
{
// Starting in Dev16, the ILocalRegistry service behind this call is free-threaded, and since the service is offered by msenv.dll can be requested
// without any marshalling (explicit or otherwise) to the UI thread.
this._registryKey = VSRegistry.RegistryRoot(serviceProvider, __VsLocalRegistryType.RegType_UserSettings, writable: true);
}
private static bool TryGetKeyPathAndName(IOption option, out string path, out string key)
{
var serialization = option.StorageLocations.OfType().SingleOrDefault();
if (serialization == null)
{
path = null;
key = null;
return false;
}
else
{
// We'll just use the filesystem APIs to decompose this
path = Path.GetDirectoryName(serialization.KeyName);
key = Path.GetFileName(serialization.KeyName);
return true;
}
}
bool IOptionPersister.TryFetch(OptionKey optionKey, out object value)
{
if (!TryGetKeyPathAndName(optionKey.Option, out var path, out var key))
{
value = null;
return false;
}
lock (_gate)
{
using var subKey = this._registryKey.OpenSubKey(path);
if (subKey == null)
{
value = null;
return false;
}
// Options that are of type bool have to be serialized as integers
if (optionKey.Option.Type == typeof(bool))
{
value = subKey.GetValue(key, defaultValue: (bool)optionKey.Option.DefaultValue ? 1 : 0).Equals(1);
return true;
}
else if (optionKey.Option.Type == typeof(long))
{
var untypedValue = subKey.GetValue(key, defaultValue: optionKey.Option.DefaultValue);
switch (untypedValue)
{
case string stringValue:
{
// Due to a previous bug we were accidentally serializing longs as strings.
// Gracefully convert those back.
var suceeded = long.TryParse(stringValue, out var longValue);
value = longValue;
return suceeded;
}
case long longValue:
value = longValue;
return true;
}
}
else if (optionKey.Option.Type == typeof(int))
{
var untypedValue = subKey.GetValue(key, defaultValue: optionKey.Option.DefaultValue);
switch (untypedValue)
{
case string stringValue:
{
// Due to a previous bug we were accidentally serializing ints as strings.
// Gracefully convert those back.
var suceeded = int.TryParse(stringValue, out var intValue);
value = intValue;
return suceeded;
}
case int intValue:
value = intValue;
return true;
}
}
else
{
// Otherwise we can just store normally
value = subKey.GetValue(key, defaultValue: optionKey.Option.DefaultValue);
return true;
}
}
value = null;
return false;
}
bool IOptionPersister.TryPersist(OptionKey optionKey, object value)
{
if (this._registryKey == null)
{
throw new InvalidOperationException();
}
if (!TryGetKeyPathAndName(optionKey.Option, out var path, out var key))
{
return false;
}
lock (_gate)
{
using var subKey = this._registryKey.CreateSubKey(path);
// Options that are of type bool have to be serialized as integers
if (optionKey.Option.Type == typeof(bool))
{
subKey.SetValue(key, (bool)value ? 1 : 0, RegistryValueKind.DWord);
return true;
}
else if (optionKey.Option.Type == typeof(long))
{
subKey.SetValue(key, value, RegistryValueKind.QWord);
return true;
}
else if (optionKey.Option.Type.IsEnum)
{
// If the enum is larger than an int, store as a QWord
if (Marshal.SizeOf(Enum.GetUnderlyingType(optionKey.Option.Type)) > Marshal.SizeOf(typeof(int)))
{
subKey.SetValue(key, (long)value, RegistryValueKind.QWord);
}
else
{
subKey.SetValue(key, (int)value, RegistryValueKind.DWord);
}
return true;
}
else
{
subKey.SetValue(key, value);
return true;
}
}
}
}
}