File: SemanticTokens\AbstractSemanticTokensTests.cs
Web Access
Project: src\src\LanguageServer\ProtocolUnitTests\Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests.csproj (Microsoft.CodeAnalysis.LanguageServer.Protocol.UnitTests)
// 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.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.LanguageServer.ExternalAccess.Razor;
using Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
using Xunit.Abstractions;
using LSP = Roslyn.LanguageServer.Protocol;
 
namespace Microsoft.CodeAnalysis.LanguageServer.UnitTests.SemanticTokens;
 
public abstract class AbstractSemanticTokensTests : AbstractLanguageServerProtocolTests
{
    protected AbstractSemanticTokensTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
    {
    }
 
    private protected static IReadOnlyDictionary<string, int> GetTokenTypeToIndex(TestLspServer server)
        => SemanticTokensSchema.GetSchema(server.ClientCapabilities.HasVisualStudioLspCapability()).TokenTypeToIndex;
 
    private protected static async Task<LSP.SemanticTokens> RunGetSemanticTokensFullAsync(TestLspServer testLspServer, LSP.Location caret)
    {
        var result = await testLspServer.ExecuteRequestAsync<LSP.SemanticTokensFullParams, LSP.SemanticTokens>(LSP.Methods.TextDocumentSemanticTokensFullName,
            CreateSemanticTokensFullParams(caret), CancellationToken.None);
        Contract.ThrowIfNull(result);
        return result;
    }
 
    private protected static async Task<LSP.SemanticTokens> RunGetSemanticTokensRangeAsync(TestLspServer testLspServer, LSP.Location caret, LSP.Range range)
    {
        var result = await testLspServer.ExecuteRequestAsync<LSP.SemanticTokensRangeParams, LSP.SemanticTokens>(LSP.Methods.TextDocumentSemanticTokensRangeName,
            CreateSemanticTokensRangeParams(caret, range), CancellationToken.None);
        Contract.ThrowIfNull(result);
        return result;
    }
 
    private protected static async Task<LSP.SemanticTokens> RunGetSemanticTokensRangesAsync(TestLspServer testLspServer, LSP.Location caret, Range[] ranges)
    {
        var result = await testLspServer.ExecuteRequestAsync<SemanticTokensRangesParams, LSP.SemanticTokens>(SemanticTokensRangesHandler.SemanticRangesMethodName,
            CreateSemanticTokensRangesParams(caret, ranges!), CancellationToken.None);
        Contract.ThrowIfNull(result);
        return result;
    }
 
    private static LSP.SemanticTokensFullParams CreateSemanticTokensFullParams(LSP.Location caret)
        => new LSP.SemanticTokensFullParams
        {
            TextDocument = new LSP.TextDocumentIdentifier { Uri = caret.Uri }
        };
 
    private static LSP.SemanticTokensRangeParams CreateSemanticTokensRangeParams(LSP.Location caret, LSP.Range range)
        => new LSP.SemanticTokensRangeParams
        {
            TextDocument = new LSP.TextDocumentIdentifier { Uri = caret.Uri },
            Range = range
        };
 
    private static SemanticTokensRangesParams CreateSemanticTokensRangesParams(LSP.Location caret, Range[] ranges)
        => new SemanticTokensRangesParams
        {
            TextDocument = new LSP.TextDocumentIdentifier { Uri = caret.Uri },
            Ranges = ranges
        };
 
    // VS doesn't currently support multi-line tokens, so we want to verify that we aren't
    // returning any in the tokens array.
    private protected static async Task VerifyBasicInvariantsAndNoMultiLineTokens(TestLspServer testLspServer, int[] tokens)
    {
        var document = testLspServer.GetCurrentSolution().Projects.First().Documents.First();
        var text = await document.GetTextAsync().ConfigureAwait(false);
 
        var currentLine = 0;
        var currentChar = 0;
 
        Assert.True(tokens.Length % 5 == 0);
 
        for (var i = 0; i < tokens.Length; i += 5)
        {
            // i: line # (relative to previous line)
            // i + 1: character # (relative to start of previous token in the line or 0)
            // i + 2: token length
 
            // Gets the current absolute line index
            Assert.True(tokens[i] >= 0, "The line offset should never be negative.");
            currentLine += tokens[i];
 
            // Gets the character # relative to the start of the line
            Assert.True(tokens[i + 1] >= 0, "The character offset should never be negative.");
 
            if (tokens[i] != 0)
            {
                currentChar = tokens[i + 1];
            }
            else
            {
                currentChar += tokens[i + 1];
                Assert.True(currentChar >= 0, "The first token on the line can't be a negative position, but applying an offset took us there.");
            }
 
            // Gets the length of the token
            var tokenLength = tokens[i + 2];
            Assert.True(tokenLength >= 0, "The token cannot have a negative length.");
 
            var lineLength = text.Lines[currentLine].SpanIncludingLineBreak.Length;
 
            // If this assertion fails, we didn't break up a multi-line token properly.
            var tokenTypeToIndex = GetTokenTypeToIndex(testLspServer);
            var kind = tokenTypeToIndex.Where(kvp => kvp.Value == tokens[i + 3]).Single().Key;
 
            Assert.True(currentChar + tokenLength <= lineLength,
                $"Multi-line token of type {kind} found on line {currentLine} at character index {currentChar}. " +
                $"The token ends at index {currentChar + tokenLength}, which exceeds the line length of {lineLength}.");
        }
    }
 
    /// <summary>
    /// This converts the raw array form back to a slightly more readable form for the purposes of understanding the diff should a test
    /// fail. This groups rows by five (so that way the diff can't desynced from the start of a new token), and also replaces the token index
    /// back with the string again.
    /// </summary>
    private protected static ImmutableArray<string> ConvertToReadableFormat(
        ClientCapabilities capabilities, int[] data)
    {
        var convertedStringsBuilder = ImmutableArray.CreateBuilder<string>(data.Length / 5);
        var tokenTypeToIndex = SemanticTokensSchema.GetSchema(capabilities.HasVisualStudioLspCapability()).TokenTypeToIndex;
 
        for (var i = 0; i < data.Length; i += 5)
        {
            var kind = tokenTypeToIndex.Single(kvp => kvp.Value == data[i + 3]).Key;
 
            convertedStringsBuilder.Add($"{data[i]}, {data[i + 1]}, {data[i + 2]}, {kind}, {data[i + 4]}");
        }
 
        return convertedStringsBuilder.MoveToImmutable();
    }
}