File: Services\CohostConfigurationChangedService.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.VisualStudioCode.RazorExtension\Microsoft.VisualStudioCode.RazorExtension.csproj (Microsoft.VisualStudioCode.RazorExtension)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Composition;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost;
using Microsoft.CodeAnalysis.Razor.Logging;
using Microsoft.CodeAnalysis.Razor.Settings;
using Microsoft.CodeAnalysis.Razor.Workspaces.Settings;
using Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
 
namespace Microsoft.VisualStudioCode.RazorExtension.Endpoints;
 
[Shared]
[Export(typeof(ICohostConfigurationChangedService))]
[Export(typeof(IRazorCohostStartupService))]
[method: ImportingConstructor]
internal sealed class CohostConfigurationChangedService(
    IClientSettingsManager clientSettingsManager,
    ILoggerFactory loggerFactory) : ICohostConfigurationChangedService, IRazorCohostStartupService
{
    private readonly IClientSettingsManager _clientSettingsManager = clientSettingsManager;
    private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<CohostConfigurationChangedService>();
 
    public int Order => WellKnownStartupOrder.Default;
 
    public Task StartupAsync(VSInternalClientCapabilities clientCapabilities, RazorCohostRequestContext requestContext, CancellationToken cancellationToken)
    {
        return RefreshOptionsAsync(requestContext, cancellationToken);
    }
 
    public Task OnConfigurationChangedAsync(RazorCohostRequestContext requestContext, CancellationToken cancellationToken)
    {
        return RefreshOptionsAsync(requestContext, cancellationToken);
    }
 
    private async Task RefreshOptionsAsync(RazorCohostRequestContext requestContext, CancellationToken cancellationToken)
    {
        _logger.LogDebug($"Refreshing options from client.");
 
        var razorClientLanguageServerManager = requestContext.GetRequiredService<IRazorClientLanguageServerManager>();
 
        var configurationParams = new ConfigurationParams()
        {
            Items = [
                //TODO: new ConfigurationItem { Section = "razor.format.enable" },
                new ConfigurationItem { Section = "razor.format.code_block_brace_on_next_line" },
                new ConfigurationItem { Section = "razor.format.attribute_indent_style" },
                new ConfigurationItem { Section = "razor.completion.commit_elements_with_space" },
                // Note: VS Code settings use snake_case, so this is "auto_closing_tags" not "autoClosingTags"
                // TypeScript code in the VS Code extension converts between camelCase and snake_case
                new ConfigurationItem { Section = "html.auto_closing_tags" },
            ]
        };
 
        var options = await razorClientLanguageServerManager.SendRequestAsync<ConfigurationParams, JsonArray>(
            Methods.WorkspaceConfigurationName,
            configurationParams,
            cancellationToken).ConfigureAwait(false);
 
        var current = _clientSettingsManager.GetClientSettings().AdvancedSettings;
        var settings = UpdateSettingsFromJson(current, options);
 
        _clientSettingsManager.Update(settings);
    }
 
    private static ClientAdvancedSettings UpdateSettingsFromJson(ClientAdvancedSettings settings, JsonArray jsonArray)
    {
        return settings with
        {
            CodeBlockBraceOnNextLine = GetBooleanOptionValue(TryGetElement(jsonArray, 0), settings.CodeBlockBraceOnNextLine),
            AttributeIndentStyle = GetEnumOptionValue(TryGetElement(jsonArray, 1), settings.AttributeIndentStyle),
            CommitElementsWithSpace = GetBooleanOptionValue(TryGetElement(jsonArray, 2), settings.CommitElementsWithSpace),
            AutoClosingTags = GetBooleanOptionValue(TryGetElement(jsonArray, 3), settings.AutoClosingTags),
        };
    }
 
    private static JsonNode? TryGetElement(JsonArray jsonArray, int index)
    {
        if (index < jsonArray.Count)
        {
            return jsonArray[index];
        }
 
        return null;
    }
 
    private static bool GetBooleanOptionValue(JsonNode? jsonNode, bool defaultValue)
    {
        if (jsonNode is null)
        {
            return defaultValue;
        }
 
        return jsonNode.ToString() == "true";
    }
 
    private static T GetEnumOptionValue<T>(JsonNode? jsonNode, T defaultValue) where T : struct
    {
        if (jsonNode is null)
        {
            return defaultValue;
        }
 
        if (Enum.TryParse<T>(jsonNode.GetValue<string>(), ignoreCase: true, out var value))
        {
            return value;
        }
 
        return defaultValue;
    }
 
    public static class TestAccessor
    {
        public static ClientAdvancedSettings UpdateSettingsFromJson(ClientAdvancedSettings settigns, JsonArray jsonArray)
            => CohostConfigurationChangedService.UpdateSettingsFromJson(settigns, jsonArray);
    }
}