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> 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.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();
        }
    }
}