Switch Razor to use only semantic token range handling (#5949)

This commit is contained in:
Allison Chou 2022-01-12 17:03:48 -08:00 коммит произвёл GitHub
Родитель 27439206fb
Коммит 62a2c1d616
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
36 изменённых файлов: 225 добавлений и 912 удалений

Просмотреть файл

@ -3,8 +3,8 @@
#nullable disable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.LanguageServer;
using Microsoft.AspNetCore.Razor.LanguageServer.ProjectSystem;
using Microsoft.AspNetCore.Razor.LanguageServer.Semantic;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
using Microsoft.Extensions.DependencyInjection;
@ -34,7 +35,7 @@ namespace Microsoft.AspNetCore.Razor.Microbenchmarks.LanguageServer
private DocumentSnapshot DocumentSnapshot { get; set; }
private DocumentSnapshot UpdatedDocumentSnapshot { get; set; }
private Range Range { get; set; }
private ProjectSnapshotManagerDispatcher ProjectSnapshotManagerDispatcher { get; set; }
@ -44,7 +45,7 @@ namespace Microsoft.AspNetCore.Razor.Microbenchmarks.LanguageServer
private string TargetPath { get; set; }
[GlobalSetup(Target = nameof(RazorSemanticTokensEditAsync))]
[GlobalSetup(Target = nameof(RazorSemanticTokensRangeAsync))]
public async Task InitializeRazorSemanticAsync()
{
await EnsureServicesInitializedAsync();
@ -54,26 +55,37 @@ namespace Microsoft.AspNetCore.Razor.Microbenchmarks.LanguageServer
PagesDirectory = Path.Combine(projectRoot, "Components", "Pages");
var filePath = Path.Combine(PagesDirectory, $"SemanticTokens.razor");
TargetPath = "/Components/Pages/SemanticTokens.razor";
var updatedPath = Path.Combine(PagesDirectory, $"Append.extra");
DocumentUri = DocumentUri.File(filePath);
DocumentSnapshot = GetDocumentSnapshot(ProjectFilePath, filePath, TargetPath);
UpdatedDocumentSnapshot = GetDocumentSnapshot(ProjectFilePath, updatedPath, TargetPath);
var text = await DocumentSnapshot.GetTextAsync().ConfigureAwait(false);
Range = new Range
{
Start = new Position
{
Line = 0,
Character = 0
},
End = new Position
{
Line = text.Lines.Count - 1,
Character = text.Lines.Last().Span.Length - 1
}
};
}
[Benchmark(Description = "Razor Semantic Tokens Formatting")]
public async Task RazorSemanticTokensEditAsync()
[Benchmark(Description = "Razor Semantic Tokens Range Handling")]
public async Task RazorSemanticTokensRangeAsync()
{
var textDocumentIdentifier = new TextDocumentIdentifier(DocumentUri);
var cancellationToken = CancellationToken.None;
var firstVersion = 1;
var documentVersion = 1;
var semanticVersion = new VersionStamp();
await UpdateDocumentAsync(firstVersion, DocumentSnapshot).ConfigureAwait(false);
var fullResult = await RazorSemanticTokenService.GetSemanticTokensAsync(textDocumentIdentifier, DocumentSnapshot, firstVersion, range: null, cancellationToken).ConfigureAwait(false);
var secondVersion = 2;
await UpdateDocumentAsync(secondVersion, UpdatedDocumentSnapshot).ConfigureAwait(false);
_ = await RazorSemanticTokenService.GetSemanticTokensEditsAsync(UpdatedDocumentSnapshot, secondVersion, textDocumentIdentifier, fullResult.ResultId, cancellationToken).ConfigureAwait(false);
await UpdateDocumentAsync(documentVersion, DocumentSnapshot).ConfigureAwait(false);
await RazorSemanticTokenService.GetSemanticTokensAsync(
textDocumentIdentifier, DocumentSnapshot, documentVersion, semanticVersion, Range, cancellationToken).ConfigureAwait(false);
}
private async Task UpdateDocumentAsync(int newVersion, DocumentSnapshot documentSnapshot)
@ -121,7 +133,7 @@ namespace Microsoft.AspNetCore.Razor.Microbenchmarks.LanguageServer
}
// We can't get C# responses without significant amounts of extra work, so let's just shim it for now, any non-Null result is fine.
internal override Task<VersionedSemanticRange> GetCSharpSemanticRangesAsync(
internal override Task<SemanticRangeResponse> GetCSharpSemanticRangesAsync(
RazorCodeDocument codeDocument,
TextDocumentIdentifier textDocumentIdentifier,
Range range,
@ -129,7 +141,8 @@ namespace Microsoft.AspNetCore.Razor.Microbenchmarks.LanguageServer
CancellationToken cancellationToken,
string previousResultId = null)
{
return Task.FromResult(new VersionedSemanticRange(new List<SemanticRange>(), IsFinalizedCSharp: false));
var result = SemanticRangeResponse.Default;
return Task.FromResult(result);
}
}
}

Просмотреть файл

@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Extensions
{
internal static class RangeExtensions
{
public static readonly Range UndefinedRange = new Range
public static readonly Range UndefinedRange = new()
{
Start = new Position(-1, -1),
End = new Position(-1, -1)
@ -131,18 +131,62 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Extensions
throw new ArgumentNullException(nameof(sourceText));
}
if (range.Start.Line >= sourceText.Lines.Count)
{
throw new ArgumentOutOfRangeException($"Range start line {range.Start.Line} matches or exceeds SourceText boundary {sourceText.Lines.Count}.");
}
if (range.End.Line >= sourceText.Lines.Count)
{
throw new ArgumentOutOfRangeException($"Range end line {range.End.Line} matches or exceeds SourceText boundary {sourceText.Lines.Count}.");
}
var start = sourceText.Lines[range.Start.Line].Start + range.Start.Character;
var end = sourceText.Lines[range.End.Line].Start + range.End.Character;
var length = end - start;
if (length < 0)
{
throw new ArgumentOutOfRangeException($"{range} resolved to a negative length.");
throw new ArgumentOutOfRangeException($"{range} resolved to zero or negative length.");
}
return new TextSpan(start, length);
}
public static Language.Syntax.TextSpan AsRazorTextSpan(this Range range, SourceText sourceText)
{
if (range is null)
{
throw new ArgumentNullException(nameof(range));
}
if (sourceText is null)
{
throw new ArgumentNullException(nameof(sourceText));
}
if (range.Start.Line >= sourceText.Lines.Count)
{
throw new ArgumentOutOfRangeException($"Range start line {range.Start.Line} matches or exceeds SourceText boundary {sourceText.Lines.Count}.");
}
if (range.End.Line >= sourceText.Lines.Count)
{
throw new ArgumentOutOfRangeException($"Range end line {range.End.Line} matches or exceeds SourceText boundary {sourceText.Lines.Count}.");
}
var start = sourceText.Lines[range.Start.Line].Start + range.Start.Character;
var end = sourceText.Lines[range.End.Line].Start + range.End.Character;
var length = end - start;
if (length < 0)
{
throw new ArgumentOutOfRangeException($"{range} resolved to zero or negative length.");
}
return new Language.Syntax.TextSpan(start, length);
}
public static bool IsUndefined(this Range range)
{
if (range is null)

Просмотреть файл

@ -9,11 +9,10 @@ using Microsoft.Extensions.Logging;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using Microsoft.AspNetCore.Razor.LanguageServer.Extensions;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
{
internal class RazorSemanticTokensEndpoint : ISemanticTokensFullHandler, ISemanticTokensRangeHandler, ISemanticTokensDeltaHandler
internal class RazorSemanticTokensEndpoint : ISemanticTokensRangeHandler
{
private readonly ILogger _logger;
private readonly RazorSemanticTokensInfoService _semanticTokensInfoService;
@ -36,16 +35,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
_logger = loggerFactory.CreateLogger<RazorSemanticTokensEndpoint>();
}
public async Task<SemanticTokens?> Handle(SemanticTokensParams request, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
return await HandleAsync(request.TextDocument.Uri.GetAbsolutePath(), cancellationToken, range: null);
}
public async Task<SemanticTokens?> Handle(SemanticTokensRangeParams request, CancellationToken cancellationToken)
{
if (request is null)
@ -53,7 +42,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
throw new ArgumentNullException(nameof(request));
}
var semanticTokens = await HandleAsync(request.TextDocument, cancellationToken, request.Range);
var semanticTokens = await _semanticTokensInfoService.GetSemanticTokensAsync(request.TextDocument, request.Range, cancellationToken);
var amount = semanticTokens is null ? "no" : (semanticTokens.Data.Length / 5).ToString(Thread.CurrentThread.CurrentCulture);
_logger.LogInformation($"Returned {amount} semantic tokens for range {request.Range} in {request.TextDocument.Uri}.");
@ -61,37 +50,15 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
return semanticTokens;
}
public async Task<SemanticTokensFullOrDelta?> Handle(SemanticTokensDeltaParams request, CancellationToken cancellationToken)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
var edits = await _semanticTokensInfoService.GetSemanticTokensEditsAsync(request.TextDocument, request.PreviousResultId, cancellationToken);
return edits;
}
public SemanticTokensRegistrationOptions GetRegistrationOptions(SemanticTokensCapability capability, ClientCapabilities clientCapabilities)
{
return new SemanticTokensRegistrationOptions
{
DocumentSelector = RazorDefaults.Selector,
Full = new SemanticTokensCapabilityRequestFull
{
Delta = true,
},
Full = false,
Legend = RazorSemanticTokensLegend.Instance,
Range = false,
Range = true,
};
}
private async Task<SemanticTokens?> HandleAsync(TextDocumentIdentifier textDocument, CancellationToken cancellationToken, Range? range = null)
{
var tokens = await _semanticTokensInfoService.GetSemanticTokensAsync(textDocument, range, cancellationToken);
return tokens;
}
}
}

Просмотреть файл

@ -34,10 +34,8 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
private readonly DocumentVersionCache _documentVersionCache;
private readonly ILogger _logger;
// Maps (docURI -> (resultId -> tokens)). We cache per-doc instead of storing all tokens
// in one giant cache to improve colorization speeds when working with multiple files.
private const int MaxCachesPerDoc = 6;
private readonly MemoryCache<DocumentUri, MemoryCache<string, VersionedSemanticTokens>> _razorDocTokensCache = new();
// Caches the last response per-document to potentially save on computation costs.
private readonly MemoryCache<DocumentUri, SemanticTokensCacheResponse> _cachedResponses = new();
public DefaultRazorSemanticTokensInfoService(
ClientNotifierServiceBase languageServer,
@ -61,16 +59,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
_logger = loggerFactory.CreateLogger<DefaultRazorSemanticTokensInfoService>();
}
public override Task<SemanticTokens?> GetSemanticTokensAsync(
TextDocumentIdentifier textDocumentIdentifier,
CancellationToken cancellationToken)
{
return GetSemanticTokensAsync(textDocumentIdentifier, range: null, cancellationToken);
}
public override async Task<SemanticTokens?> GetSemanticTokensAsync(
TextDocumentIdentifier textDocumentIdentifier,
Range? range,
Range range,
CancellationToken cancellationToken)
{
var documentPath = textDocumentIdentifier.Uri.GetAbsolutePath();
@ -87,17 +78,37 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
var (documentSnapshot, documentVersion) = documentInfo.Value;
var tokens = await GetSemanticTokensAsync(textDocumentIdentifier, documentSnapshot, documentVersion, range, cancellationToken);
var semanticVersion = await GetDocumentSemanticVersionAsync(documentSnapshot).ConfigureAwait(false);
// If we have a matching cached response, avoid computation and return early.
if (_cachedResponses.TryGetValue(textDocumentIdentifier.Uri, out var response) &&
response.IsCSharpFinalized &&
response.SemanticVersion == semanticVersion &&
response.Range == range)
{
return response.SemanticTokens;
}
var tokens = await GetSemanticTokensAsync(
textDocumentIdentifier, documentSnapshot, documentVersion, semanticVersion, range, cancellationToken);
return tokens;
}
private static async Task<VersionStamp> GetDocumentSemanticVersionAsync(DocumentSnapshot documentSnapshot)
{
var documentVersionStamp = await documentSnapshot.GetTextVersionAsync();
var semanticVersion = documentVersionStamp.GetNewerVersion(documentSnapshot.Project.Version);
return semanticVersion;
}
// Internal for testing
internal async Task<SemanticTokens?> GetSemanticTokensAsync(
TextDocumentIdentifier textDocumentIdentifier,
DocumentSnapshot documentSnapshot,
int documentVersion,
Range? range,
VersionStamp semanticVersion,
Range range,
CancellationToken cancellationToken)
{
var codeDocument = await GetRazorCodeDocumentAsync(documentSnapshot);
@ -109,13 +120,12 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
cancellationToken.ThrowIfCancellationRequested();
var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument, range);
IReadOnlyList<SemanticRange>? csharpSemanticRanges = null;
string? newResultId = null;
var isFinalizedCSharp = false;
var isCSharpFinalized = false;
try
{
(csharpSemanticRanges, isFinalizedCSharp) = await GetCSharpSemanticRangesAsync(
codeDocument, textDocumentIdentifier, range, documentVersion, cancellationToken);
(csharpSemanticRanges, isCSharpFinalized) = await GetCSharpSemanticRangesAsync(
codeDocument, textDocumentIdentifier, range, documentVersion, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
@ -132,153 +142,14 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
return null;
}
var semanticVersion = await GetDocumentSemanticVersionAsync(documentSnapshot);
var data = ConvertSemanticRangesToSemanticTokensData(combinedSemanticRanges, codeDocument);
var tokens = new SemanticTokens { Data = data };
if (newResultId is null)
{
// If there's no C# in the Razor doc, we won't have a resultId returned to us.
// Just use a GUID instead.
newResultId = Guid.NewGuid().ToString();
}
// Cache the result so we can potentially avoid recomputation next time around.
var cacheResponse = new SemanticTokensCacheResponse(semanticVersion, range, tokens, isCSharpFinalized);
_cachedResponses.Set(textDocumentIdentifier.Uri, cacheResponse);
var razorSemanticTokens = ConvertSemanticRangesToSemanticTokens(
combinedSemanticRanges, codeDocument, newResultId, isFinalizedCSharp);
UpdateRazorDocCache(textDocumentIdentifier.Uri, semanticVersion, newResultId, razorSemanticTokens);
return new SemanticTokens { ResultId = razorSemanticTokens.ResultId, Data = razorSemanticTokens.Data.ToImmutableArray() };
}
public override async Task<SemanticTokensFullOrDelta?> GetSemanticTokensEditsAsync(
TextDocumentIdentifier textDocumentIdentifier,
string? previousResultId,
CancellationToken cancellationToken)
{
var documentPath = textDocumentIdentifier.Uri.GetAbsolutePath();
if (documentPath is null)
{
return null;
}
var documentInfo = await TryGetDocumentInfoAsync(documentPath, cancellationToken).ConfigureAwait(false);
if (documentInfo is null)
{
return null;
}
var (documentSnapshot, documentVersion) = documentInfo.Value;
return await GetSemanticTokensEditsAsync(
documentSnapshot,
documentVersion,
textDocumentIdentifier,
previousResultId,
cancellationToken);
}
// Internal for testing
internal async Task<SemanticTokensFullOrDelta?> GetSemanticTokensEditsAsync(
DocumentSnapshot documentSnapshot,
long documentVersion,
TextDocumentIdentifier textDocumentIdentifier,
string? previousResultId,
CancellationToken cancellationToken)
{
VersionStamp? cachedSemanticVersion = null;
IReadOnlyList<int>? previousResults = null;
var csharpTokensFinalized = false;
// Attempting to retrieve cached tokens for the Razor document.
if (previousResultId != null &&
_razorDocTokensCache.TryGetValue(textDocumentIdentifier.Uri, out var documentCache) &&
documentCache.TryGetValue(previousResultId, out var cachedTokens))
{
previousResults = cachedTokens?.SemanticTokens;
cachedSemanticVersion = cachedTokens?.SemanticVersion;
if (cachedTokens is not null)
{
csharpTokensFinalized = cachedTokens.IsFinalizedCSharp;
}
}
var semanticVersion = await GetDocumentSemanticVersionAsync(documentSnapshot);
cancellationToken.ThrowIfCancellationRequested();
// We have to recompute tokens in two scenarios:
// 1) SemanticVersion is different. Occurs if there's been any text edits to the
// Razor file or ProjectVersion has changed.
// 2) C# returned non-finalized tokens to us the last time around. May occur if a
// partial compilation was used to compute tokens.
if (semanticVersion == default || cachedSemanticVersion != semanticVersion || !csharpTokensFinalized)
{
var codeDocument = await GetRazorCodeDocumentAsync(documentSnapshot);
if (codeDocument is null)
{
throw new ArgumentNullException(nameof(codeDocument));
}
cancellationToken.ThrowIfCancellationRequested();
var razorSemanticRanges = TagHelperSemanticRangeVisitor.VisitAllNodes(codeDocument);
var (csharpSemanticRanges, isFinalizedCSharp) = await GetCSharpSemanticRangesAsync(
codeDocument,
textDocumentIdentifier,
range: null,
documentVersion,
cancellationToken,
previousResultId);
var combinedSemanticRanges = CombineSemanticRanges(razorSemanticRanges, csharpSemanticRanges);
// We return null when we have an incomplete view of the document.
// Likely CSharp ahead of us in terms of document versions.
// We return null (which to the LSP is a no-op) to prevent flashing of CSharp elements.
if (combinedSemanticRanges is null)
{
return null;
}
var newResultId = Guid.NewGuid().ToString();
var newTokens = ConvertSemanticRangesToSemanticTokens(
combinedSemanticRanges, codeDocument, newResultId, isFinalizedCSharp);
UpdateRazorDocCache(textDocumentIdentifier.Uri, semanticVersion, newResultId, newTokens);
if (previousResults is null)
{
return new SemanticTokens { ResultId = newTokens.ResultId, Data = newTokens.Data.ToImmutableArray() };
}
var razorSemanticEdits = SemanticTokensEditsDiffer.ComputeSemanticTokensEdits(newTokens, previousResults);
return razorSemanticEdits;
}
else
{
var result = new SemanticTokensFullOrDelta(new SemanticTokensDelta
{
Edits = Array.Empty<SemanticTokensEdit>(),
ResultId = previousResultId,
});
return result;
}
}
private void UpdateRazorDocCache(
DocumentUri documentUri,
VersionStamp semanticVersion,
string newResultId,
SemanticTokensResponse newTokens)
{
// Update the tokens cache associated with the given Razor doc. If the doc has no
// associated cache, we will create one.
if (!_razorDocTokensCache.TryGetValue(documentUri, out var documentCache))
{
documentCache = new MemoryCache<string, VersionedSemanticTokens>(sizeLimit: MaxCachesPerDoc);
_razorDocTokensCache.Set(documentUri, documentCache);
}
documentCache.Set(newResultId, new VersionedSemanticTokens(semanticVersion, newTokens.Data, newTokens.IsFinalizedCSharp));
return tokens;
}
private static async Task<RazorCodeDocument?> GetRazorCodeDocumentAsync(DocumentSnapshot documentSnapshot)
@ -318,30 +189,33 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
}
// Internal and virtual for testing only
internal virtual async Task<VersionedSemanticRange> GetCSharpSemanticRangesAsync(
internal virtual async Task<SemanticRangeResponse> GetCSharpSemanticRangesAsync(
RazorCodeDocument codeDocument,
TextDocumentIdentifier textDocumentIdentifier,
Range? range,
Range razorRange,
long documentVersion,
CancellationToken cancellationToken,
string? previousResultId = null)
{
var razorRanges = new List<SemanticRange>();
if (!TryGetMinimalCSharpRange(codeDocument, out var csharpRange))
// We'll try to call into the mapping service to map to the projected range for us. If that doesn't work,
// we'll try to find the minimal range ourselves.
if (!_documentMappingService.TryMapToProjectedDocumentRange(codeDocument, razorRange, out var csharpRange) &&
!TryGetMinimalCSharpRange(codeDocument, razorRange, out csharpRange))
{
return new VersionedSemanticRange(razorRanges, IsFinalizedCSharp: true);
return SemanticRangeResponse.Default;
}
var csharpResponse = await GetMatchingCSharpResponseAsync(textDocumentIdentifier, documentVersion, csharpRange, cancellationToken);
// Indicates an issue with retrieving the C# response (e.g. no response or C# is out of sync with us).
// Unrecoverable, return null to indicate no change. It will retry in a bit.
// Unrecoverable, return default to indicate no change. It will retry in a bit.
if (csharpResponse is null)
{
return VersionedSemanticRange.Default;
return SemanticRangeResponse.Default;
}
var razorRanges = new List<SemanticRange>();
SemanticRange? previousSemanticRange = null;
for (var i = 0; i < csharpResponse.Data.Length; i += 5)
{
@ -355,61 +229,66 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
lineDelta, charDelta, length, tokenType, tokenModifiers, previousSemanticRange);
if (_documentMappingService.TryMapFromProjectedDocumentRange(codeDocument, semanticRange.Range, out var originalRange))
{
var razorRange = new SemanticRange(semanticRange.Kind, originalRange, tokenModifiers);
if (range is null || range.OverlapsWith(razorRange.Range))
var razorSemanticRange = new SemanticRange(semanticRange.Kind, originalRange, tokenModifiers);
if (razorRange is null || razorRange.OverlapsWith(razorSemanticRange.Range))
{
razorRanges.Add(razorRange);
razorRanges.Add(razorSemanticRange);
}
}
previousSemanticRange = semanticRange;
}
var result = razorRanges.ToImmutableList();
return new VersionedSemanticRange(result, csharpResponse.IsFinalizedCSharp);
var result = razorRanges.ToArray();
var semanticRanges = new SemanticRangeResponse(result, IsCSharpFinalized: csharpResponse.IsCSharpFinalized);
return semanticRanges;
}
// Internal for testing only
internal static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, [NotNullWhen(true)] out Range? range)
private static bool TryGetMinimalCSharpRange(RazorCodeDocument codeDocument, Range razorRange, [NotNullWhen(true)] out Range? csharpRange)
{
var minIndex = -1;
var maxIndex = -1;
SourceSpan? minGeneratedSpan = null;
SourceSpan? maxGeneratedSpan = null;
var sourceText = codeDocument.GetSourceText();
var textSpan = razorRange.AsTextSpan(sourceText);
var csharpDoc = codeDocument.GetCSharpDocument();
// If there aren't any source mappings, there's no C# code in the Razor doc.
if (csharpDoc.SourceMappings.Count == 0)
{
range = null;
return false;
}
// We only need to colorize the portions of the generated doc that correspond with a C# mapping.
// To accomplish this while minimizing the amount of work we need to do, we'll only colorize the
// range spanning from the first C# span to the last C# span.
SourceSpan? minSpan = null;
SourceSpan? maxSpan = null;
// We want to find the min and max C# source mapping that corresponds with our Razor range.
foreach (var mapping in csharpDoc.SourceMappings)
{
var generatedSpan = mapping.GeneratedSpan;
if (minSpan is null || generatedSpan.AbsoluteIndex < minSpan.Value.AbsoluteIndex)
{
minSpan = generatedSpan;
}
var mappedTextSpan = mapping.OriginalSpan.AsTextSpan();
if (maxSpan is null || generatedSpan.AbsoluteIndex + generatedSpan.Length > maxSpan.Value.AbsoluteIndex + maxSpan.Value.Length)
if (textSpan.OverlapsWith(mappedTextSpan))
{
maxSpan = generatedSpan;
if (minIndex == -1 || mapping.OriginalSpan.AbsoluteIndex < minIndex)
{
minIndex = mapping.OriginalSpan.AbsoluteIndex;
minGeneratedSpan = mapping.GeneratedSpan;
}
if (maxIndex == -1 || mapping.OriginalSpan.AbsoluteIndex + mapping.OriginalSpan.Length > maxIndex)
{
maxIndex = mapping.OriginalSpan.AbsoluteIndex + mapping.OriginalSpan.Length;
maxGeneratedSpan = mapping.GeneratedSpan;
}
}
}
var csharpSourceText = codeDocument.GetCSharpSourceText();
var start = csharpSourceText.Lines.GetLinePosition(minSpan!.Value.AbsoluteIndex);
var startPosition = new Position(start.Line, start.Character);
// Create a new projected range based on our calculated min/max source spans.
if (minGeneratedSpan is not null && maxGeneratedSpan is not null)
{
var csharpSourceText = codeDocument.GetCSharpSourceText();
var startRange = minGeneratedSpan.Value.AsTextSpan().AsRange(csharpSourceText);
var endRange = maxGeneratedSpan.Value.AsTextSpan().AsRange(csharpSourceText);
var end = csharpSourceText.Lines.GetLinePosition(maxSpan!.Value.AbsoluteIndex + maxSpan!.Value.Length);
var endPosition = new Position(end.Line, end.Character);
csharpRange = new Range { Start = startRange.Start, End = endRange.End };
return true;
}
range = new Range(startPosition, endPosition);
return true;
csharpRange = null;
return false;
}
private async Task<SemanticTokensResponse?> GetMatchingCSharpResponseAsync(
@ -433,8 +312,8 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
return null;
}
// C# doesn't return resultIds so we can return null here.
return new SemanticTokensResponse(ResultId: null, csharpResponse.Tokens ?? Array.Empty<int>(), csharpResponse.IsFinalized);
var response = new SemanticTokensResponse(csharpResponse.Tokens ?? Array.Empty<int>(), csharpResponse.IsFinalized);
return response;
}
private static SemanticRange DataToSemanticRange(
@ -465,11 +344,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
return semanticRange;
}
private static SemanticTokensResponse ConvertSemanticRangesToSemanticTokens(
private static ImmutableArray<int> ConvertSemanticRangesToSemanticTokensData(
IReadOnlyList<SemanticRange> semanticRanges,
RazorCodeDocument razorCodeDocument,
string? resultId,
bool isFinalizedCSharp)
RazorCodeDocument razorCodeDocument)
{
SemanticRange? previousResult = null;
@ -482,16 +359,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
previousResult = result;
}
var tokensResult = new SemanticTokensResponse(resultId, data.ToArray(), isFinalizedCSharp);
return tokensResult;
}
private static async Task<VersionStamp> GetDocumentSemanticVersionAsync(DocumentSnapshot documentSnapshot)
{
var documentVersionStamp = await documentSnapshot.GetTextVersionAsync();
var semanticVersion = documentVersionStamp.GetNewerVersion(documentSnapshot.Project.Version);
return semanticVersion;
return data.ToImmutableArray();
}
/**
@ -554,18 +422,17 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
}, cancellationToken);
}
// Internal for testing
internal record VersionedSemanticRange(IReadOnlyList<SemanticRange>? SemanticRanges, bool IsFinalizedCSharp)
private record SemanticTokensResponse(int[] Data, bool IsCSharpFinalized)
{
public static VersionedSemanticRange Default => new(null, false);
public static SemanticTokensResponse Default => new(Array.Empty<int>(), false);
}
private record VersionedSemanticTokens(VersionStamp? SemanticVersion, IReadOnlyList<int> SemanticTokens, bool IsFinalizedCSharp);
// Internal for testing
internal record SemanticTokensResponse(string? ResultId, int[] Data, bool IsFinalizedCSharp)
internal record SemanticRangeResponse(SemanticRange[]? SemanticRanges, bool IsCSharpFinalized)
{
public static SemanticTokensResponse Default => new(null, Array.Empty<int>(), false);
public static SemanticRangeResponse Default => new(null, false);
}
private record SemanticTokensCacheResponse(VersionStamp SemanticVersion, Range Range, SemanticTokens SemanticTokens, bool IsCSharpFinalized);
}
}

Просмотреть файл

@ -9,10 +9,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
{
internal abstract class RazorSemanticTokensInfoService
{
public abstract Task<SemanticTokens?> GetSemanticTokensAsync(TextDocumentIdentifier textDocumentIdentifier, CancellationToken cancellationToken);
public abstract Task<SemanticTokens?> GetSemanticTokensAsync(TextDocumentIdentifier textDocumentIdentifier, Range? range, CancellationToken cancellationToken);
public abstract Task<SemanticTokensFullOrDelta?> GetSemanticTokensEditsAsync(TextDocumentIdentifier textDocumentIdentifier, string? previousId, CancellationToken cancellationToken);
public abstract Task<SemanticTokens?> GetSemanticTokensAsync(TextDocumentIdentifier textDocumentIdentifier, Range range, CancellationToken cancellationToken);
}
}

Просмотреть файл

@ -1,127 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using static Microsoft.AspNetCore.Razor.LanguageServer.Semantic.DefaultRazorSemanticTokensInfoService;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
{
internal class SemanticTokensEditsDiffer : TextDiffer
{
private SemanticTokensEditsDiffer(IReadOnlyList<int> oldArray, int[] newArray)
{
if (oldArray is null)
{
throw new ArgumentNullException(nameof(oldArray));
}
OldArray = oldArray;
NewArray = newArray;
}
private IReadOnlyList<int> OldArray { get; }
private int[] NewArray { get; }
protected override int OldTextLength => OldArray.Count;
protected override int NewTextLength => NewArray.Length;
protected override bool ContentEquals(int oldTextIndex, int newTextIndex)
{
return OldArray[oldTextIndex] == NewArray[newTextIndex];
}
public static SemanticTokensFullOrDelta ComputeSemanticTokensEdits(
SemanticTokensResponse newTokens,
IReadOnlyList<int> previousResults)
{
var differ = new SemanticTokensEditsDiffer(previousResults, newTokens.Data);
var diffs = differ.ComputeDiff();
var edits = differ.ProcessEdits(diffs);
var result = new SemanticTokensDelta
{
ResultId = newTokens.ResultId,
Edits = edits,
};
return result;
}
private Container<SemanticTokensEdit> ProcessEdits(IReadOnlyList<DiffEdit> diffs)
{
var razorResults = new List<RazorSemanticTokensEdit>();
foreach (var diff in diffs)
{
var current = razorResults.Count > 0 ? razorResults[razorResults.Count - 1] : null;
switch (diff.Operation)
{
case DiffEdit.Type.Delete:
if (current != null &&
current.Start + current.DeleteCount == diff.Position)
{
current.DeleteCount += 1;
}
else
{
razorResults.Add(new RazorSemanticTokensEdit
{
Start = diff.Position,
DeleteCount = 1,
});
}
break;
case DiffEdit.Type.Insert:
if (current != null &&
current.Data != null &&
current.Data.Count > 0 &&
current.Start == diff.Position)
{
current.Data.Add(NewArray[diff.NewTextPosition!.Value]);
}
else
{
var semanticTokensEdit = new RazorSemanticTokensEdit
{
Start = diff.Position,
Data = new List<int>
{
NewArray[diff.NewTextPosition!.Value],
},
DeleteCount = 0,
};
razorResults.Add(semanticTokensEdit);
}
break;
}
}
var results = razorResults.Select(e => e.ToSemanticTokensEdit());
return results.ToList();
}
// We need to have a shim class because SemanticTokensEdit.Data is Immutable, so if we operate on it directly then every time we append an element we're allocating an entire new array.
// In some large (but not implausibly so) copy-paste scenarios that can cause long delays and large allocations.
private class RazorSemanticTokensEdit
{
public int Start { get; set; }
public int DeleteCount { get; set; }
public IList<int>? Data { get; set; }
// Since we need to add to the Data object during ProcessEdits but return an "ImmutableArray" in the end lets wait until the end to convert.
public SemanticTokensEdit ToSemanticTokensEdit()
{
return new SemanticTokensEdit
{
Data = Data?.ToImmutableArray(),
Start = Start,
DeleteCount = DeleteCount,
};
}
}
}
}

Просмотреть файл

@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.LanguageServer.Extensions;
using Microsoft.AspNetCore.Razor.LanguageServer.Semantic.Models;
using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
@ -17,18 +18,23 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
{
private readonly List<SemanticRange> _semanticRanges;
private readonly RazorCodeDocument _razorCodeDocument;
private readonly Range? _range;
private TagHelperSemanticRangeVisitor(RazorCodeDocument razorCodeDocument, Range? range)
private TagHelperSemanticRangeVisitor(RazorCodeDocument razorCodeDocument, TextSpan? range) : base(range)
{
_semanticRanges = new List<SemanticRange>();
_razorCodeDocument = razorCodeDocument;
_range = range;
}
public static IReadOnlyList<SemanticRange> VisitAllNodes(RazorCodeDocument razorCodeDocument, Range? range = null)
{
var visitor = new TagHelperSemanticRangeVisitor(razorCodeDocument, range);
TextSpan? rangeAsTextSpan = null;
if (range is not null)
{
var sourceText = razorCodeDocument.GetSourceText();
rangeAsTextSpan = range.AsRazorTextSpan(sourceText);
}
var visitor = new TagHelperSemanticRangeVisitor(razorCodeDocument, rangeAsTextSpan);
visitor.Visit(razorCodeDocument.GetSyntaxTree().Root);
@ -478,12 +484,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Semantic
void AddRange(SemanticRange semanticRange)
{
if (_range is null || semanticRange.Range.OverlapsWith(_range))
if (semanticRange.Range.Start != semanticRange.Range.End)
{
if (semanticRange.Range.Start != semanticRange.Range.End)
{
_semanticRanges.Add(semanticRange);
}
_semanticRanges.Add(semanticRange);
}
}
}

Просмотреть файл

@ -3,6 +3,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Language;
@ -21,7 +23,6 @@ using OmniSharp.Extensions.JsonRpc;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using Xunit;
using OmniSharpRange = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;
using Range = OmniSharp.Extensions.LanguageServer.Protocol.Models.Range;
namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Semantic
{
@ -45,21 +46,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Semantic
await AssertSemanticTokensAsync(txt, isRazor: false, csharpTokens: cSharpResponse, documentMappings: null, documentVersion: 1);
}
[Fact]
public async Task GetSemanticTokensEdits_CSharp_RazorIfNotReady()
{
var txt = $@"<p></p>@{{
var d = ""t"";
}}";
var cSharpResponse = new ProvideSemanticTokensResponse(
tokens: Array.Empty<int>(), isFinalized: true, hostDocumentSyncVersion: 0);
var isRazor = true;
var response = await AssertSemanticTokensAsync(txt, isRazor, csharpTokens: cSharpResponse, documentVersion: 0);
_ = await AssertSemanticTokenEditsAsync(txt, expectDelta: true, isRazor, response.Item1, response.Item2);
}
[Fact]
public async Task GetSemanticTokens_CSharpBlock_HTML()
{
@ -278,68 +264,6 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Semantic
await AssertSemanticTokensAsync(txt, isRazor: false, csharpTokens: cSharpResponse, documentMappings: mappings);
}
[Fact]
public async Task GetSemanticTokens_CSharp_UsesCache()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}@{{ var d = }}";
var csharpTokens = new int[]
{
14, 12, 3, RazorSemanticTokensLegend.CSharpKeyword, 0,
13, 15, 1, RazorSemanticTokensLegend.CSharpVariable, 0,
12, 25, 1, RazorSemanticTokensLegend.CSharpOperator, 0,
11, 10, 25, RazorSemanticTokensLegend.CSharpKeyword, 0, // No mapping
};
var cSharpResponse = new ProvideSemanticTokensResponse(csharpTokens, isFinalized: true, hostDocumentSyncVersion: 0);
var mappings = new (OmniSharpRange, OmniSharpRange?)[] {
(new OmniSharpRange(new Position(14, 12), new Position(14, 15)), new OmniSharpRange(new Position(1, 3), new Position(1, 6))),
(new OmniSharpRange(new Position(27, 15), new Position(27, 16)), new OmniSharpRange(new Position(1, 7), new Position(1, 8))),
(new OmniSharpRange(new Position(39, 25), new Position(39, 26)), new OmniSharpRange(new Position(1, 9), new Position(1, 10))),
(new OmniSharpRange(new Position(50, 10), new Position(50, 35)), null)
};
var isRazor = false;
var (previousResultId, service, mockClient, document) = await AssertSemanticTokensAsync(txt, isRazor, csharpTokens: cSharpResponse, documentMappings: mappings);
await AssertSemanticTokenEditsAsync(txt: null, expectDelta: true, isRazor, previousResultId: previousResultId, service: service);
mockClient.Verify(l => l.SendRequestAsync(LanguageServerConstants.RazorProvideSemanticTokensRangeEndpoint, It.IsAny<SemanticTokensParams>()), Times.Once());
}
[Fact]
public async Task GetSemanticTokens_CSharp_RequeueOnPartialTokens()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}@{{ var d = }}";
var csharpTokens = new int[]
{
14, 12, 3, RazorSemanticTokensLegend.CSharpKeyword, 0,
13, 15, 1, RazorSemanticTokensLegend.CSharpVariable, 0,
12, 25, 1, RazorSemanticTokensLegend.CSharpOperator, 0,
11, 10, 25, RazorSemanticTokensLegend.CSharpKeyword, 0, // No mapping
};
var csharpResponse = new ProvideSemanticTokensResponse(csharpTokens, isFinalized: false, hostDocumentSyncVersion: 0);
var mappings = new (OmniSharpRange, OmniSharpRange?)[] {
(new OmniSharpRange(new Position(14, 12), new Position(14, 15)), new OmniSharpRange(new Position(1, 3), new Position(1, 6))),
(new OmniSharpRange(new Position(27, 15), new Position(27, 16)), new OmniSharpRange(new Position(1, 7), new Position(1, 8))),
(new OmniSharpRange(new Position(39, 25), new Position(39, 26)), new OmniSharpRange(new Position(1, 9), new Position(1, 10))),
(new OmniSharpRange(new Position(50, 10), new Position(50, 35)), null)
};
var csharpFinalizedResponse = new ProvideSemanticTokensResponse(
tokens: Array.Empty<int>(), isFinalized: true, hostDocumentSyncVersion: 0);
var isRazor = false;
var (previousResultId, service, mockClient, document) = await AssertSemanticTokensAsync(
txt, isRazor, csharpTokens: csharpResponse, documentMappings: mappings);
await AssertSemanticTokenEditsAsync(txt: null, expectDelta: true, isRazor, previousResultId, service: service);
mockClient.Verify(l => l.SendRequestAsync(LanguageServerConstants.RazorProvideSemanticTokensRangeEndpoint, It.IsAny<ProvideSemanticTokensRangeParams>()), Times.Exactly(2));
}
#endregion
#region HTML
@ -547,12 +471,12 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Semantic
await AssertSemanticTokensAsync(txt, isRazor: true);
}
[Fact]
[Fact(Skip = "https://github.com/dotnet/razor-tooling/issues/5948")]
public async Task GetSemanticTokens_Razor_InRangeAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var startIndex = txt.IndexOf("test1", StringComparison.Ordinal);
var startIndex = txt.IndexOf("test1", StringComparison.Ordinal); ;
var endIndex = startIndex + 5;
var codeDocument = CreateCodeDocument(txt, DefaultTagHelpers);
@ -564,7 +488,7 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.Test.Semantic
var endPosition = new Position(endLine, endChar);
var location = new OmniSharpRange(startPosition, endPosition);
await AssertSemanticTokensAsync(txt, isRazor: false, location: location);
await AssertSemanticTokensAsync(txt, isRazor: false, range: location);
}
#endregion DirectiveAttributes
@ -665,190 +589,29 @@ slf*@";
await AssertSemanticTokensAsync(txt, isRazor: false);
}
[Fact]
public async Task GetSemanticTokens_Razor_EditTwiceAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1>";
var secondTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1>T</test1>";
var thirdTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1>Test</test1>";
var isRazor = true;
var (firstResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, secondTxt, thirdTxt }, new bool[] { isRazor, isRazor, isRazor });
var (secondResultId, _, _) = await AssertSemanticTokenEditsAsync(secondTxt, expectDelta: true, isRazor, previousResultId: firstResultId, service);
var (_, _, _) = await AssertSemanticTokenEditsAsync(thirdTxt, expectDelta: true, isRazor, previousResultId: secondResultId, service);
}
[Fact]
public async Task GetSemanticTokens_Razor_NoDifferenceAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var isRazor = false;
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(txt, isRazor);
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(txt, expectDelta: true, isRazor, previousResultId: previousResultId, service: service);
Assert.Equal(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_RemoveTokensAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1><test1></test1><test1></test1> ";
var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var isRazor = false;
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, newTxt }, new bool[] { isRazor, isRazor });
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(newTxt, expectDelta: true, isRazor, previousResultId: previousResultId, service);
Assert.NotEqual(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_OnlyDifferences_AppendAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1 bool-val='true'></test1> ";
var isRazor = false;
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, newTxt }, new bool[] { isRazor, isRazor });
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(newTxt, expectDelta: true, isRazor, previousResultId: previousResultId, service: service);
Assert.NotEqual(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_CoalesceDeleteAndAddAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1 /> ";
var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}{Environment.NewLine}<p @minimized /> ";
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, newTxt }, new bool[] { false, true });
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(newTxt, expectDelta: true, isRazor: false, previousResultId: previousResultId, service: service);
Assert.NotEqual(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_OriginallyNone_ThenSomeAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}";
var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var isRazor = false;
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, newTxt }, new bool[] { isRazor, isRazor });
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(newTxt, expectDelta: true, isRazor, previousResultId: previousResultId, service);
Assert.NotEqual(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_GetEditsWithNoPreviousAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var (previousResultId, _, _) = await AssertSemanticTokenEditsAsync(txt, expectDelta: false, isRazor: false, previousResultId: null);
Assert.NotNull(previousResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_SomeTagHelpers_ThenNoneAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<p></p> ";
var isRazor = false;
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, newTxt }, new bool[] { isRazor, isRazor });
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(newTxt, expectDelta: true, isRazor, previousResultId: previousResultId, service);
Assert.NotEqual(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_OnlyDifferences_InternalAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1><test1></test1> ";
var isRazor = false;
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, newTxt }, new bool[] { isRazor, isRazor });
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(newTxt, expectDelta: true, isRazor, previousResultId: previousResultId, service);
Assert.NotEqual(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_ModifyAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}";
var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}" +
$"<test1 bool-va=\"true\" />{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}" +
$"<test1 bool-val=\"true\" />{Environment.NewLine}";
var isRazor = false;
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, newTxt }, new bool[] { isRazor, isRazor });
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(newTxt, expectDelta: true, isRazor, previousResultId: previousResultId, service: service);
Assert.NotEqual(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_Razor_OnlyDifferences_NewLinesAsync()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1> ";
var newTxt = $"@addTagHelper *, TestAssembly{Environment.NewLine}<test1></test1>{Environment.NewLine}" +
$"<test1></test1> ";
var isRazor = false;
var (previousResultId, service, _, _) = await AssertSemanticTokensAsync(new string[] { txt, newTxt }, new bool[] { isRazor, isRazor });
var (newResultId, _, _) = await AssertSemanticTokenEditsAsync(newTxt, expectDelta: true, isRazor, previousResultId: previousResultId, service);
Assert.NotEqual(previousResultId, newResultId);
}
[Fact]
public async Task GetSemanticTokens_CSharp_TryGetMinimalCSharpRange()
{
var txt = $"@addTagHelper *, TestAssembly{Environment.NewLine}@{{ var d = }}";
var (documentSnapshots, _) = CreateDocumentSnapshot(new string[] { txt }, new bool[] { false }, DefaultTagHelpers);
var snapshot = documentSnapshots.Dequeue();
var csharpDoc = await snapshot.GetGeneratedOutputAsync();
DefaultRazorSemanticTokensInfoService.TryGetMinimalCSharpRange(csharpDoc, out var actualRange);
var expectedRange = new Range
{
Start = new Position(line: 12, character: 38),
End = new Position(line: 29, character: 11)
};
Assert.Equal(expectedRange, actualRange);
}
private Task<(string?, RazorSemanticTokensInfoService, Mock<ClientNotifierServiceBase>, Queue<DocumentSnapshot>)> AssertSemanticTokensAsync(
string txt,
bool isRazor,
OmniSharpRange? range = null,
RazorSemanticTokensInfoService? service = null,
OmniSharpRange? location = null,
ProvideSemanticTokensResponse? csharpTokens = null,
(OmniSharpRange, OmniSharpRange?)[]? documentMappings = null,
int? documentVersion = 0)
{
return AssertSemanticTokensAsync(new string[] { txt }, new bool[] { isRazor }, service, location, csharpTokens, documentMappings, documentVersion);
if (range is null)
{
var lines = txt.Split(Environment.NewLine);
range = new OmniSharpRange { Start = new Position { Line = 0, Character = 0 }, End = new Position { Line = lines.Length - 1, Character = lines[^1].Length } };
};
return AssertSemanticTokensAsync(new string[] { txt }, new bool[] { isRazor }, range, service, csharpTokens, documentMappings, documentVersion);
}
private async Task<(string?, RazorSemanticTokensInfoService, Mock<ClientNotifierServiceBase>, Queue<DocumentSnapshot>)> AssertSemanticTokensAsync(
string[] txtArray,
bool[] isRazorArray,
OmniSharpRange range,
RazorSemanticTokensInfoService? service = null,
OmniSharpRange? location = null,
ProvideSemanticTokensResponse? csharpTokens = null,
(OmniSharpRange, OmniSharpRange?)[]? documentMappings = null,
int? documentVersion = 0)
@ -869,7 +632,8 @@ slf*@";
if (service is null)
{
(service, serviceMock) = GetDefaultRazorSemanticTokenInfoService(documentSnapshots, csharpTokens, documentMappings, documentVersion);
(service, serviceMock) = await GetDefaultRazorSemanticTokenInfoServiceAsync(
documentSnapshots, csharpTokens, documentMappings, documentVersion).ConfigureAwait(false);
}
var outService = service;
@ -877,7 +641,7 @@ slf*@";
var textDocumentIdentifier = textDocumentIdentifiers.Dequeue();
// Act
var tokens = await service.GetSemanticTokensAsync(textDocumentIdentifier, location, CancellationToken.None);
var tokens = await service.GetSemanticTokensAsync(textDocumentIdentifier, range, CancellationToken.None);
// Assert
AssertSemanticTokensMatchesBaseline(tokens?.Data);
@ -885,45 +649,7 @@ slf*@";
return (tokens?.ResultId, outService, serviceMock!, documentSnapshots);
}
private async Task<(string?, RazorSemanticTokensInfoService, Mock<ClientNotifierServiceBase>)> AssertSemanticTokenEditsAsync(
string? txt,
bool expectDelta,
bool isRazor,
string? previousResultId,
RazorSemanticTokensInfoService? service = null,
long? documentVersion = 0)
{
// Arrange
var cSharpTokens = new ProvideSemanticTokensResponse(tokens: null, isFinalized: true, documentVersion);
Mock<ClientNotifierServiceBase>? clientMock = null;
if (service is null)
{
var (documentSnapshots, _) = CreateDocumentSnapshot(new string?[] { txt }, new bool[] { isRazor }, DefaultTagHelpers);
(service, clientMock) = GetDefaultRazorSemanticTokenInfoService(documentSnapshots, cSharpTokens);
}
var textDocumentIdentifier = GetIdentifier(isRazor);
// Act
var edits = await service.GetSemanticTokensEditsAsync(textDocumentIdentifier, previousResultId, CancellationToken.None);
// Assert
if (expectDelta)
{
AssertSemanticTokensEditsMatchesBaseline(edits!);
return (edits!.Delta!.ResultId, service, clientMock!);
}
else
{
AssertSemanticTokensMatchesBaseline(edits!.Full!.Data);
return (edits.Full.ResultId, service, clientMock!);
}
}
private (RazorSemanticTokensInfoService, Mock<ClientNotifierServiceBase>) GetDefaultRazorSemanticTokenInfoService(
private async Task<(RazorSemanticTokensInfoService, Mock<ClientNotifierServiceBase>)> GetDefaultRazorSemanticTokenInfoServiceAsync(
Queue<DocumentSnapshot> documentSnapshots,
ProvideSemanticTokensResponse? csharpTokens = null,
(OmniSharpRange, OmniSharpRange?)[]? documentMappings = null,
@ -939,7 +665,7 @@ slf*@";
.Setup(l => l.SendRequestAsync(LanguageServerConstants.RazorProvideSemanticTokensRangeEndpoint, It.IsAny<SemanticTokensParams>()))
.Returns(Task.FromResult(responseRouterReturns.Object));
var documentMappingService = new Mock<RazorDocumentMappingService>(MockBehavior.Strict);
if (documentMappings != null)
if (documentMappings is not null)
{
foreach (var (cSharpRange, razorRange) in documentMappings)
{
@ -950,6 +676,25 @@ slf*@";
}
}
foreach (var snapshot in documentSnapshots)
{
var codeDocument = await snapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
if (codeDocument is not null)
{
var sourceText = codeDocument.GetSourceText();
var lastLine = sourceText.Lines.Last();
var projectedRange = new OmniSharpRange
{
Start = new Position(0, 0),
End = new Position(sourceText.Lines.Count - 1, character: lastLine.Span.Length),
};
documentMappingService
.Setup(s => s.TryMapToProjectedDocumentRange(codeDocument, It.IsAny<OmniSharpRange>(), out projectedRange))
.Returns(true);
}
}
var loggingFactory = new Mock<LoggerFactory>(MockBehavior.Strict);
loggingFactory.Protected().Setup("CheckDisposed").CallBase();

Просмотреть файл

@ -1,11 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 91 0 //markupTagDelimiter
0 1 1 92 0 //markupElement
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 92 0 //markupElement
0 1 1 91 0 //markupTagDelimiter
0 1 1 84 0 //razorTransition
0 1 1 84 0 //razorTransition
2 0 1 84 0 //razorTransition

Просмотреть файл

@ -1,9 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 84 0 //razorTransition
0 1 1 84 0 //razorTransition
0 2 3 15 0 //keyword
0 4 1 8 0 //variable
0 2 1 21 0 //operator
0 2 1 84 0 //razorTransition

Просмотреть файл

@ -1,9 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 84 0 //razorTransition
0 1 1 84 0 //razorTransition
0 2 3 15 0 //keyword
0 4 1 8 0 //variable
0 2 1 21 0 //operator
0 2 1 84 0 //razorTransition

Просмотреть файл

@ -1,7 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 6 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,7 +0,0 @@
Delta
10 0 [ 2 0 ]
11 0 [ 91 ]
12 0 [ 0 1 ]
13 1 [ 92 0 0 2 1 84 ]
17 2 [ 9 86 ]
21 1 [ 10 ]

Просмотреть файл

@ -1,10 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 92 0 //markupElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 92 0 //markupElement
0 5 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,10 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,27 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 6 8 83 0 //razorTagHelperAttribute
0 8 1 93 0 //markupOperator
0 1 1 95 0 //markupAttributeQuote
0 5 1 95 0 //markupAttributeQuote
0 2 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 6 8 83 0 //razorTagHelperAttribute
0 8 1 93 0 //markupOperator
0 1 1 95 0 //markupAttributeQuote
0 5 1 95 0 //markupAttributeQuote
0 2 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 6 8 83 0 //razorTagHelperAttribute
0 8 1 93 0 //markupOperator
0 1 1 95 0 //markupAttributeQuote
0 5 1 95 0 //markupAttributeQuote
0 2 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,4 +0,0 @@
Delta
22 2 [ 7 94 ]
26 1 [ 7 ]
36 1 [ 1 4 95 0 0 4 ]

Просмотреть файл

@ -1,10 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,10 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,3 +0,0 @@
Delta
21 0 [ 6 8 83 0 0 8 1 93 0 0 1 1 95 0 0 ]
22 0 [ 1 95 0 0 1 ]

Просмотреть файл

@ -1,10 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,2 +0,0 @@
Delta
45 0 [ 0 1 1 91 0 0 1 5 82 0 0 5 1 91 0 0 1 1 91 0 0 1 1 91 0 0 1 5 82 0 0 5 1 91 0 ]

Просмотреть файл

@ -1,10 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,2 +0,0 @@
Delta
45 0 [ 1 0 1 91 0 0 1 5 82 0 0 5 1 91 0 0 1 1 91 0 0 1 1 91 0 0 1 5 82 0 0 5 1 91 0 ]

Просмотреть файл

@ -1,3 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective

Просмотреть файл

@ -1,2 +0,0 @@
Delta
10 0 [ 1 0 1 91 0 0 1 5 82 0 0 5 1 91 0 0 1 1 91 0 0 1 1 91 0 0 1 5 82 0 0 5 1 91 0 ]

Просмотреть файл

@ -1,24 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter

Просмотреть файл

@ -1,10 +0,0 @@
//line,characterPos,length,tokenType,modifier
0 0 1 84 0 //razorTransition
0 1 12 87 0 //razorDirective
1 0 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 1 91 0 //markupTagDelimiter
0 1 5 82 0 //razorTagHelperElement
0 5 1 91 0 //markupTagDelimiter