File: EmbeddedLanguages\Json\LanguageServices\JsonBraceMatcher.cs
Web Access
Project: src\src\roslyn\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Composition;
using System.Threading;
using Microsoft.CodeAnalysis.BraceMatching;
using Microsoft.CodeAnalysis.EmbeddedLanguages;
using Microsoft.CodeAnalysis.EmbeddedLanguages.Common;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages.Json.LanguageServices;

using JsonToken = EmbeddedSyntaxToken<JsonKind>;

/// <summary>
/// Brace matcher impl for embedded json strings.
/// </summary>
[ExportEmbeddedLanguageBraceMatcher(
    PredefinedEmbeddedLanguageNames.Json,
    [LanguageNames.CSharp, LanguageNames.VisualBasic],
    supportsUnannotatedAPIs: true,
    "Json"), Shared]
internal sealed class JsonBraceMatcher : IEmbeddedLanguageBraceMatcher
{
    [ImportingConstructor]
    [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
    public JsonBraceMatcher()
    {
    }

    public BraceMatchingResult? FindBraces(
        Project project,
        SemanticModel semanticModel,
        SyntaxToken token,
        int position,
        BraceMatchingOptions options,
        CancellationToken cancellationToken)
    {
        if (!options.HighlightingOptions.HighlightRelatedJsonComponentsUnderCursor)
            return null;

        var info = project.GetRequiredLanguageService<IEmbeddedLanguagesProvider>().EmbeddedLanguageInfo;
        var detector = JsonLanguageDetector.GetOrCreate(semanticModel.Compilation, info);

        // We do support brace matching in strings that look very likely to be json, even if we aren't 100% certain
        // if it truly is json.
        var tree = detector.TryParseString(token, semanticModel, includeProbableStrings: true, cancellationToken);
        if (tree == null)
            return null;

        return GetMatchingBraces(tree, position);
    }

    private static BraceMatchingResult? GetMatchingBraces(JsonTree tree, int position)
    {
        var virtualChar = tree.Text.Find(position);
        if (virtualChar == null)
            return null;

        var ch = virtualChar.Value;
        return ch.Value is '{' or '[' or '(' or '}' or ']' or ')'
            ? FindBraceHighlights(tree, ch)
            : null;
    }

    private static BraceMatchingResult? FindBraceHighlights(JsonTree tree, VirtualChar ch)
        => FindBraceMatchingResult(tree.Root, ch);

    private static BraceMatchingResult? FindBraceMatchingResult(JsonNode node, VirtualChar ch)
    {
        var fullSpan = node.GetFullSpan();
        if (fullSpan == null)
            return null;

        if (!fullSpan.Value.Contains(ch.Span.Start))
            return null;

        switch (node)
        {
            case JsonArrayNode array when Matches(array.OpenBracketToken, array.CloseBracketToken, ch):
                return Create(array.OpenBracketToken, array.CloseBracketToken);

            case JsonObjectNode obj when Matches(obj.OpenBraceToken, obj.CloseBraceToken, ch):
                return Create(obj.OpenBraceToken, obj.CloseBraceToken);

            case JsonConstructorNode cons when Matches(cons.OpenParenToken, cons.CloseParenToken, ch):
                return Create(cons.OpenParenToken, cons.CloseParenToken);
        }

        foreach (var child in node)
        {
            if (child.IsNode)
            {
                var result = FindBraceMatchingResult(child.Node, ch);
                if (result != null)
                    return result;
            }
        }

        return null;
    }

    private static BraceMatchingResult? Create(JsonToken open, JsonToken close)
       => open.IsMissing || close.IsMissing
           ? null
           : new BraceMatchingResult(open.GetSpan(), close.GetSpan());

    private static bool Matches(JsonToken openToken, JsonToken closeToken, VirtualChar ch)
        => openToken.VirtualChars.Contains(ch) || closeToken.VirtualChars.Contains(ch);
}