|
// 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.Collections.Immutable;
using System.Diagnostics;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ImplementType;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CommonLanguageServerProtocol.Framework;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
using LSP = Roslyn.LanguageServer.Protocol;
namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Configuration
{
[Method(Methods.WorkspaceDidChangeConfigurationName)]
internal partial class DidChangeConfigurationNotificationHandler : ILspServiceNotificationHandler<LSP.DidChangeConfigurationParams>, IOnInitialized
{
private bool _supportWorkspaceConfiguration;
private readonly ILspLogger _lspLogger;
private readonly IGlobalOptionService _globalOptionService;
private readonly IClientLanguageServerManager _clientLanguageServerManager;
private readonly Guid _registrationId;
/// <summary>
/// All the <see cref="ConfigurationItem.Section"/> needs to be refreshed from the client.
/// </summary>
private readonly ImmutableArray<ConfigurationItem> _configurationItems;
/// <summary>
/// The matching option and its language name needs to be refreshed. The order matches <see cref="_configurationItems"/> sent to the client.
/// LanguageName would be null if the option is <see cref="ISingleValuedOption"/>.
/// </summary>
private readonly ImmutableArray<(IOption2 option, string? lanugageName)> _optionsAndLanguageNamesToRefresh;
private static readonly ImmutableDictionary<string, string> s_languageNameToPrefix = ImmutableDictionary<string, string>.Empty
.Add(LanguageNames.CSharp, "csharp")
.Add(LanguageNames.VisualBasic, "visual_basic");
public static readonly ImmutableArray<string> SupportedLanguages = [LanguageNames.CSharp, LanguageNames.VisualBasic];
public DidChangeConfigurationNotificationHandler(
ILspLogger logger,
IGlobalOptionService globalOptionService,
IClientLanguageServerManager clientLanguageServerManager)
{
_lspLogger = logger;
_globalOptionService = globalOptionService;
_clientLanguageServerManager = clientLanguageServerManager;
_registrationId = Guid.NewGuid();
_configurationItems = GenerateGlobalConfigurationItems();
_optionsAndLanguageNamesToRefresh = GenerateOptionsNeedsToRefresh();
RoslynDebug.Assert(_configurationItems.Length == _optionsAndLanguageNamesToRefresh.Length);
}
public bool MutatesSolutionState => true;
public bool RequiresLSPSolution => false;
public Task HandleNotificationAsync(DidChangeConfigurationParams request, RequestContext requestContext, CancellationToken cancellationToken)
=> RefreshOptionsAsync(cancellationToken);
private async Task RefreshOptionsAsync(CancellationToken cancellationToken)
{
// We rely on the workspace/configuration to get the option values. If client doesn't support this, don't update.
if (!_supportWorkspaceConfiguration)
{
return;
}
var configurationsFromClient = await GetConfigurationsAsync(cancellationToken).ConfigureAwait(false);
if (configurationsFromClient.IsEmpty)
{
// Failed to get values from client, do nothing.
return;
}
// We always fetch VB and C# value from client if the option is IPerLanguageValuedOption.
RoslynDebug.Assert(configurationsFromClient.Length == SupportedOptions.Sum(option => option is IPerLanguageValuedOption ? 2 : 1));
// LSP ensures the order of result from client should match the order we sent from server.
using var _ = ArrayBuilder<KeyValuePair<OptionKey2, object?>>.GetInstance(out var optionsToUpdate);
for (var i = 0; i < configurationsFromClient.Length; i++)
{
var valueFromClient = configurationsFromClient[i];
var (option, languageName) = _optionsAndLanguageNamesToRefresh[i];
// If option doesn't exist in the client, don't try to update the option.
if (!string.IsNullOrEmpty(valueFromClient))
{
if (option.Definition.Serializer.TryParse(valueFromClient, out var parsedValue))
{
optionsToUpdate.Add(KeyValuePairUtil.Create(new OptionKey2(option, language: languageName), parsedValue));
}
else
{
_lspLogger.LogWarning($"Failed to parse '{valueFromClient}' to type: '{option.Type.Name}'. '{option.Name}' would not be updated.");
}
}
}
_globalOptionService.SetGlobalOptions(optionsToUpdate.ToImmutable());
}
private async Task<ImmutableArray<string?>> GetConfigurationsAsync(CancellationToken cancellationToken)
{
// Attempt to get configurations from the client. If this throws we'll get NFW reports.
var configurationParams = new ConfigurationParams() { Items = _configurationItems.AsArray() };
var options = await _clientLanguageServerManager.SendRequestAsync<ConfigurationParams, JsonArray>(
Methods.WorkspaceConfigurationName, configurationParams, cancellationToken).ConfigureAwait(false);
// Failed to get result from client.
Contract.ThrowIfNull(options);
var converted = options.SelectAsArray(token => token?.ToString());
return converted;
}
private static ImmutableArray<(IOption2 option, string? langaugeName)> GenerateOptionsNeedsToRefresh()
{
using var _ = ArrayBuilder<(IOption2, string?)>.GetInstance(out var builder);
foreach (var option in SupportedOptions)
{
if (option is IPerLanguageValuedOption)
{
foreach (var language in SupportedLanguages)
{
builder.Add((option, language));
}
}
else
{
builder.Add((option, null));
}
}
return builder.ToImmutableAndClear();
}
/// <summary>
/// Generate the configuration items send to the client.
/// For each option, generate its full name. If the option is <see cref="ISingleValuedOption"/> it's is what we sent to the client.
/// If it is <see cref="IPerLanguageValuedOption"/>, then generate two configurationItems with prefix visual_basic and csharp.
/// </summary>
private static ImmutableArray<ConfigurationItem> GenerateGlobalConfigurationItems()
{
using var _ = ArrayBuilder<ConfigurationItem>.GetInstance(out var builder);
foreach (var option in SupportedOptions)
{
var fullOptionName = GenerateFullNameForOption(option);
if (option is IPerLanguageValuedOption)
{
foreach (var language in SupportedLanguages)
{
builder.Add(new ConfigurationItem()
{
Section = string.Concat(s_languageNameToPrefix[language], '|', fullOptionName),
});
}
}
else
{
builder.Add(new ConfigurationItem()
{
Section = fullOptionName,
});
}
}
return builder.ToImmutableAndClear();
}
/// <summary>
/// Generate the full name of <param name="option"/>.
/// It would be in the format like {optionGroupName}.{OptionName}
/// </summary>
/// <remarks>
/// Example:Full name of <see cref="ImplementTypeOptionsStorage.InsertionBehavior"/> would be:
/// implement_type.dotnet_member_insertion_location
/// </remarks>
internal static string GenerateFullNameForOption(IOption2 option)
{
var optionGroupName = GenerateOptionGroupName(option);
// All options send to the client should have group name and config name.
RoslynDebug.Assert(!string.IsNullOrEmpty(optionGroupName));
RoslynDebug.Assert(!string.IsNullOrEmpty(option.Definition.ConfigName));
return string.Concat(optionGroupName, '.', option.Definition.ConfigName);
}
private static string GenerateOptionGroupName(IOption2 option)
{
using var pooledStack = SharedPools.Default<Stack<string>>().GetPooledObject();
var stack = pooledStack.Object;
// Get the full name of option group, we are at the tail now, so use a stack to reverse it.
var optionGroup = option.Definition.Group;
while (optionGroup != null && optionGroup.Name != null)
{
stack.Push(optionGroup.Name);
optionGroup = optionGroup.Parent;
}
using var _ = PooledStringBuilder.GetInstance(out var stringBuilder);
while (!stack.IsEmpty())
{
if (stringBuilder.Length > 0)
{
stringBuilder.Append('.');
}
stringBuilder.Append(stack.Pop());
}
return stringBuilder.ToString();
}
}
}
|