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