// 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.Generic;
using System.ComponentModel;
using System.ComponentModel.Composition;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.LanguageServices.Setup;
using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.Options
{
///
/// Serializes settings marked with to and from the user's roaming profile.
///
[Export(typeof(IOptionPersister))]
internal sealed class RoamingVisualStudioProfileOptionPersister : ForegroundThreadAffinitizedObject, IOptionPersister
{
// NOTE: This service is not public or intended for use by teams/individuals outside of Microsoft. Any data stored is subject to deletion without warning.
[Guid("9B164E40-C3A2-4363-9BC5-EB4039DEF653")]
private class SVsSettingsPersistenceManager { };
private readonly ISettingsManager _settingManager;
private readonly IGlobalOptionService _globalOptionService;
///
/// The list of options that have been been fetched from , by key. We track this so
/// if a later change happens, we know to refresh that value. This is synchronized with monitor locks on
/// .
///
private readonly Dictionary> _optionsToMonitorForChanges = new();
private readonly object _optionsToMonitorForChangesGate = new();
///
/// We make sure this code is from the UI by asking for all in
///
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public RoamingVisualStudioProfileOptionPersister(IThreadingContext threadingContext, IGlobalOptionService globalOptionService, [Import(typeof(SVsServiceProvider))] IServiceProvider serviceProvider)
: base(threadingContext, assertIsForeground: true) // The GetService call requires being on the UI thread or else it will marshal and risk deadlock
{
Contract.ThrowIfNull(globalOptionService);
_settingManager = (ISettingsManager)serviceProvider.GetService(typeof(SVsSettingsPersistenceManager));
_globalOptionService = globalOptionService;
// While the settings persistence service should be available in all SKUs it is possible an ISO shell author has undefined the
// contributing package. In that case persistence of settings won't work (we don't bother with a backup solution for persistence
// as the scenario seems exceedingly unlikely), but we shouldn't crash the IDE.
if (_settingManager != null)
{
var settingsSubset = _settingManager.GetSubset("*");
settingsSubset.SettingChangedAsync += OnSettingChangedAsync;
}
}
private System.Threading.Tasks.Task OnSettingChangedAsync(object sender, PropertyChangedEventArgs args)
{
List optionsToRefresh = null;
lock (_optionsToMonitorForChangesGate)
{
if (_optionsToMonitorForChanges.TryGetValue(args.PropertyName, out var optionsToRefreshInsideLock))
{
// Make a copy of the list so we aren't using something that might mutate underneath us.
optionsToRefresh = optionsToRefreshInsideLock.ToList();
}
}
if (optionsToRefresh != null)
{
// Refresh the actual options outside of our _optionsToMonitorForChangesGate so we avoid any deadlocks by calling back
// into the global option service under our lock. There isn't some race here where if we were fetching an option for the first time
// while the setting was changed we might not refresh it. Why? We call RecordObservedValueToWatchForChanges before we fetch the value
// and since this event is raised after the setting is modified, any new setting would have already been observed in GetFirstOrDefaultValue.
// And if it wasn't, this event will then refresh it.
foreach (var optionToRefresh in optionsToRefresh)
{
if (TryFetch(optionToRefresh, out var optionValue))
{
_globalOptionService.RefreshOption(optionToRefresh, optionValue);
}
}
}
return System.Threading.Tasks.Task.CompletedTask;
}
private object GetFirstOrDefaultValue(OptionKey optionKey, IEnumerable roamingSerializations)
{
// There can be more than 1 roaming location in the order of their priority.
// When fetching a value, we iterate all of them until we find the first one that exists.
// When persisting a value, we always use the first location.
// This functionality exists for breaking changes to persistence of some options. In such a case, there
// will be a new location added to the beginning with a new name. When fetching a value, we might find the old
// location (and can upgrade the value accordingly) but we only write to the new location so that
// we don't interfere with older versions. This will essentially "fork" the user's options at the time of upgrade.
foreach (var roamingSerialization in roamingSerializations)
{
var storageKey = roamingSerialization.GetKeyNameForLanguage(optionKey.Language);
RecordObservedValueToWatchForChanges(optionKey, storageKey);
if (_settingManager.TryGetValue(storageKey, out object value) == GetValueResult.Success)
{
return value;
}
}
return optionKey.Option.DefaultValue;
}
public bool TryFetch(OptionKey optionKey, out object value)
{
if (_settingManager == null)
{
Debug.Fail("Manager field is unexpectedly null.");
value = null;
return false;
}
// Do we roam this at all?
var roamingSerializations = optionKey.Option.StorageLocations.OfType();
if (!roamingSerializations.Any())
{
value = null;
return false;
}
value = GetFirstOrDefaultValue(optionKey, roamingSerializations);
// VS's ISettingsManager has some quirks around storing enums. Specifically,
// it *can* persist and retrieve enums, but only if you properly call
// GetValueOrDefault. This is because it actually stores enums just
// as ints and depends on the type parameter passed in to convert the integral
// value back to an enum value. Unfortunately, we call GetValueOrDefault