diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/ClassifiedSpanVisitor.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/ClassifiedSpanVisitor.cs index 1dd004a2062..f1bcacd6b96 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/ClassifiedSpanVisitor.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/ClassifiedSpanVisitor.cs @@ -3,16 +3,18 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; namespace Microsoft.AspNetCore.Razor.Language; internal class ClassifiedSpanVisitor : SyntaxWalker { private readonly RazorSourceDocument _source; - private readonly List _spans; + private readonly ImmutableArray.Builder _spans; private readonly Action _baseVisitCSharpCodeBlock; private readonly Action _baseVisitCSharpStatement; @@ -29,15 +31,10 @@ internal class ClassifiedSpanVisitor : SyntaxWalker private BlockKindInternal _currentBlockKind; private SyntaxNode? _currentBlock; - public ClassifiedSpanVisitor(RazorSourceDocument source) + private ClassifiedSpanVisitor(RazorSourceDocument source, ImmutableArray.Builder spans) { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } - _source = source; - _spans = new List(); + _spans = spans; _baseVisitCSharpCodeBlock = base.VisitCSharpCodeBlock; _baseVisitCSharpStatement = base.VisitCSharpStatement; @@ -54,7 +51,15 @@ public ClassifiedSpanVisitor(RazorSourceDocument source) _currentBlockKind = BlockKindInternal.Markup; } - public IReadOnlyList ClassifiedSpans => _spans; + public static ImmutableArray VisitRoot(RazorSyntaxTree syntaxTree) + { + using var _ = ArrayBuilderPool.GetPooledObject(out var builder); + + var visitor = new ClassifiedSpanVisitor(syntaxTree.Source, builder); + visitor.Visit(syntaxTree.Root); + + return builder.DrainToImmutable(); + } public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node) { diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/ItemCollection.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/ItemCollection.cs index 8b943c1c90b..26cf810a8d9 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/ItemCollection.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/ItemCollection.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.AspNetCore.Razor.Language; @@ -128,4 +129,17 @@ void ICollection.CopyTo(Array array, int index) { ((ICollection)_inner).CopyTo(array, index); } + + internal bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + where TKey : notnull + { + if (!_inner.TryGetValue(key, out var objValue)) + { + value = default; + return false; + } + + value = (TValue)objValue; + return true; + } } diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/Legacy/RazorSyntaxTreeExtensions.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/Legacy/RazorSyntaxTreeExtensions.cs index 6e939d670fa..67b03324925 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/Legacy/RazorSyntaxTreeExtensions.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/Legacy/RazorSyntaxTreeExtensions.cs @@ -1,38 +1,30 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System; -using System.Collections.Generic; +using System.Collections.Immutable; namespace Microsoft.AspNetCore.Razor.Language.Legacy; internal static class RazorSyntaxTreeExtensions { - public static IReadOnlyList GetClassifiedSpans(this RazorSyntaxTree syntaxTree) + public static ImmutableArray GetClassifiedSpans(this RazorSyntaxTree syntaxTree) { if (syntaxTree == null) { throw new ArgumentNullException(nameof(syntaxTree)); } - var visitor = new ClassifiedSpanVisitor(syntaxTree.Source); - visitor.Visit(syntaxTree.Root); - - return visitor.ClassifiedSpans; + return ClassifiedSpanVisitor.VisitRoot(syntaxTree); } - public static IReadOnlyList GetTagHelperSpans(this RazorSyntaxTree syntaxTree) + public static ImmutableArray GetTagHelperSpans(this RazorSyntaxTree syntaxTree) { if (syntaxTree == null) { throw new ArgumentNullException(nameof(syntaxTree)); } - var visitor = new TagHelperSpanVisitor(syntaxTree.Source); - visitor.Visit(syntaxTree.Root); - - return visitor.TagHelperSpans; + return TagHelperSpanVisitor.VisitRoot(syntaxTree); } } diff --git a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/TagHelperSpanVisitor.cs b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/TagHelperSpanVisitor.cs index 9b9532991f7..25f138c9111 100644 --- a/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/TagHelperSpanVisitor.cs +++ b/src/Compiler/Microsoft.AspNetCore.Razor.Language/src/TagHelperSpanVisitor.cs @@ -1,26 +1,33 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - -using System.Collections.Generic; +using System.Collections.Immutable; using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; namespace Microsoft.AspNetCore.Razor.Language; internal class TagHelperSpanVisitor : SyntaxWalker { private readonly RazorSourceDocument _source; - private readonly List _spans; + private readonly ImmutableArray.Builder _spans; - public TagHelperSpanVisitor(RazorSourceDocument source) + private TagHelperSpanVisitor(RazorSourceDocument source, ImmutableArray.Builder spans) { _source = source; - _spans = new List(); + _spans = spans; } - public IReadOnlyList TagHelperSpans => _spans; + public static ImmutableArray VisitRoot(RazorSyntaxTree syntaxTree) + { + using var _ = ArrayBuilderPool.GetPooledObject(out var builder); + + var visitor = new TagHelperSpanVisitor(syntaxTree.Source, builder); + visitor.Visit(syntaxTree.Root); + + return builder.DrainToImmutable(); + } public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node) { diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorDocumentMappingService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorDocumentMappingService.cs index aec69f3c0ba..83468bd3245 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorDocumentMappingService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorDocumentMappingService.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -12,6 +13,7 @@ using Microsoft.AspNetCore.Razor.Language.Legacy; using Microsoft.AspNetCore.Razor.LanguageServer.Extensions; using Microsoft.AspNetCore.Razor.LanguageServer.Protocol; +using Microsoft.AspNetCore.Razor.PooledObjects; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CodeAnalysis.Razor.Workspaces.Extensions; using Microsoft.CodeAnalysis.Text; @@ -491,13 +493,14 @@ public async Task RemapWorkspaceEditAsync(WorkspaceEdit workspace // Internal for testing internal static RazorLanguageKind GetLanguageKindCore( - IReadOnlyList classifiedSpans, - IReadOnlyList tagHelperSpans, + ImmutableArray classifiedSpans, + ImmutableArray tagHelperSpans, int hostDocumentIndex, int hostDocumentLength, bool rightAssociative) { - for (var i = 0; i < classifiedSpans.Count; i++) + var length = classifiedSpans.Length; + for (var i = 0; i < length; i++) { var classifiedSpan = classifiedSpans[i]; var span = classifiedSpan.Span; @@ -522,7 +525,7 @@ internal static RazorLanguageKind GetLanguageKindCore( // of, if we're also at the start of the next one if (rightAssociative) { - if (i < classifiedSpans.Count - 1 && classifiedSpans[i + 1].Span.AbsoluteIndex == hostDocumentIndex) + if (i < classifiedSpans.Length - 1 && classifiedSpans[i + 1].Span.AbsoluteIndex == hostDocumentIndex) { // If we're at the start of the next span, then use that span return GetLanguageFromClassifiedSpan(classifiedSpans[i + 1]); @@ -538,9 +541,8 @@ internal static RazorLanguageKind GetLanguageKindCore( } } - for (var i = 0; i < tagHelperSpans.Count; i++) + foreach (var tagHelperSpan in tagHelperSpans) { - var tagHelperSpan = tagHelperSpans[i]; var span = tagHelperSpan.Span; if (span.AbsoluteIndex <= hostDocumentIndex) @@ -562,7 +564,7 @@ internal static RazorLanguageKind GetLanguageKindCore( // Use the language of the last classified span if we're at the end // of the document. - if (classifiedSpans.Count != 0 && hostDocumentIndex == hostDocumentLength) + if (classifiedSpans.Length != 0 && hostDocumentIndex == hostDocumentLength) { var lastClassifiedSpan = classifiedSpans.Last(); return GetLanguageFromClassifiedSpan(lastClassifiedSpan); @@ -952,19 +954,15 @@ private static SourceText GetGeneratedSourceText(IRazorGeneratedDocument generat return codeDocument.GetGeneratedSourceText(generatedDocument); } - private static IReadOnlyList GetClassifiedSpans(RazorCodeDocument document) + private static ImmutableArray GetClassifiedSpans(RazorCodeDocument document) { // Since this service is called so often, we get a good performance improvement by caching these values // for this code document. If the document changes, as the user types, then the document instance will be // different, so we don't need to worry about invalidating the cache. - var classifiedSpans = (IReadOnlyList)document.Items[typeof(ClassifiedSpanInternal)]; - if (classifiedSpans is null) + if (!document.Items.TryGetValue(typeof(ClassifiedSpanInternal), out ImmutableArray classifiedSpans)) { var syntaxTree = document.GetSyntaxTree(); - - var visitor = new ClassifiedSpanVisitor(syntaxTree.Source); - visitor.Visit(syntaxTree.Root); - classifiedSpans = visitor.ClassifiedSpans; + classifiedSpans = ClassifiedSpanVisitor.VisitRoot(syntaxTree); document.Items[typeof(ClassifiedSpanInternal)] = classifiedSpans; } @@ -972,19 +970,15 @@ private static IReadOnlyList GetClassifiedSpans(RazorCod return classifiedSpans; } - private static IReadOnlyList GetTagHelperSpans(RazorCodeDocument document) + private static ImmutableArray GetTagHelperSpans(RazorCodeDocument document) { // Since this service is called so often, we get a good performance improvement by caching these values // for this code document. If the document changes, as the user types, then the document instance will be // different, so we don't need to worry about invalidating the cache. - var tagHelperSpans = (IReadOnlyList)document.Items[typeof(TagHelperSpanInternal)]; - if (tagHelperSpans is null) + if (!document.Items.TryGetValue(typeof(TagHelperSpanInternal), out ImmutableArray tagHelperSpans)) { var syntaxTree = document.GetSyntaxTree(); - - var visitor = new TagHelperSpanVisitor(syntaxTree.Source); - visitor.Visit(syntaxTree.Root); - tagHelperSpans = visitor.TagHelperSpans; + tagHelperSpans = TagHelperSpanVisitor.VisitRoot(syntaxTree); document.Items[typeof(TagHelperSpanInternal)] = tagHelperSpans; } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/TagHelperDeltaResult.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/TagHelperDeltaResult.cs index a33c170f8ff..173de6322be 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/TagHelperDeltaResult.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.ProjectEngineHost/Serialization/TagHelperDeltaResult.cs @@ -26,7 +26,7 @@ public ImmutableArray Apply(ImmutableArray.GetPooledObject(out var newTagHelpers); - newTagHelpers.SetCapacityIfNeeded(baseTagHelpers.Length + Added.Length - Removed.Length); + newTagHelpers.SetCapacityIfLarger(baseTagHelpers.Length + Added.Length - Removed.Length); newTagHelpers.AddRange(Added); foreach (var existingTagHelper in baseTagHelpers) diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorDocumentMappingServiceTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorDocumentMappingServiceTest.cs index 533b6874f2f..4030fbda7d2 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorDocumentMappingServiceTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/RazorDocumentMappingServiceTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language.CodeGeneration; @@ -868,22 +869,18 @@ public void GetLanguageKindCore_GetsLastClassifiedSpanLanguageIfAtEndOfDocument( { // Arrange var text = $"Something{Environment.NewLine}"; - var classifiedSpans = new List() - { - new ClassifiedSpanInternal( - new SourceSpan(0, 0), + var classifiedSpans = ImmutableArray.Create( + new(new SourceSpan(0, 0), blockSpan: new SourceSpan(absoluteIndex: 0, lineIndex: 0, characterIndex: 0, length: text.Length), SpanKindInternal.Transition, blockKind: default, acceptedCharacters: default), - new ClassifiedSpanInternal( - new SourceSpan(0, 26), + new(new SourceSpan(0, 26), blockSpan: default, SpanKindInternal.Markup, blockKind: default, - acceptedCharacters: default) - }; - var tagHelperSpans = Array.Empty(); + acceptedCharacters: default)); + var tagHelperSpans = ImmutableArray.Empty; // Act var languageKind = RazorDocumentMappingService.GetLanguageKindCore(classifiedSpans, tagHelperSpans, text.Length, text.Length, rightAssociative: false); @@ -1068,7 +1065,7 @@ public void GetLanguageKindCore_TagHelperInCSharpRightAssociative() Assert.Equal(RazorLanguageKind.Html, languageKind); } - private static (IReadOnlyList classifiedSpans, IReadOnlyList tagHelperSpans) GetClassifiedSpans(string text, IReadOnlyList? tagHelpers = null) + private static (ImmutableArray classifiedSpans, ImmutableArray tagHelperSpans) GetClassifiedSpans(string text, IReadOnlyList? tagHelpers = null) { var codeDocument = CreateCodeDocument(text, tagHelpers); var syntaxTree = codeDocument.GetSyntaxTree(); diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs index c3beccc1120..92023edd2e6 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/ImmutableArrayExtensions.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. +using Microsoft.AspNetCore.Razor.PooledObjects; + namespace System.Collections.Immutable; /// @@ -16,11 +18,70 @@ public static ImmutableArray NullToEmpty(this ImmutableArray array) return array.IsDefault ? ImmutableArray.Empty : array; } - public static void SetCapacityIfNeeded(this ImmutableArray.Builder builder, int newCapacity) + public static void SetCapacityIfLarger(this ImmutableArray.Builder builder, int newCapacity) { if (builder.Capacity < newCapacity) { builder.Capacity = newCapacity; } } + + /// + /// Returns the current contents as an and sets + /// the collection to a zero length array. + /// + /// + /// If equals + /// , the internal array will be extracted + /// as an without copying the contents. Otherwise, the + /// contents will be copied into a new array. The collection will then be set to a + /// zero-length array. + /// + /// An immutable array. + public static ImmutableArray DrainToImmutable(this ImmutableArray.Builder builder) + { +#if NET8_0_OR_GREATER + return builder.DrainToImmutable(); +#else + if (builder.Count == 0) + { + return ImmutableArray.Empty; + } + + if (builder.Count == builder.Capacity) + { + return builder.MoveToImmutable(); + } + + var result = builder.ToImmutable(); + builder.Clear(); + return result; +#endif + } + + public static ImmutableArray SelectAsArray(this ImmutableArray source, Func selector) + { + return source switch + { + [] => ImmutableArray.Empty, + [var item] => ImmutableArray.Create(selector(item)), + [var item1, var item2] => ImmutableArray.Create(selector(item1), selector(item2)), + [var item1, var item2, var item3] => ImmutableArray.Create(selector(item1), selector(item2), selector(item3)), + [var item1, var item2, var item3, var item4] => ImmutableArray.Create(selector(item1), selector(item2), selector(item3), selector(item4)), + var items => BuildResult(items, selector) + }; + + static ImmutableArray BuildResult(ImmutableArray items, Func selector) + { + using var _ = ArrayBuilderPool.GetPooledObject(out var result); + result.SetCapacityIfLarger(items.Length); + + foreach (var item in items) + { + result.Add(selector(item)); + } + + return result.DrainToImmutable(); + } + } } diff --git a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs index b88dda7d0e8..480d35f8d31 100644 --- a/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs +++ b/src/Shared/Microsoft.AspNetCore.Razor.Utilities.Shared/PooledObjects/PooledArrayBuilder`1.cs @@ -75,7 +75,7 @@ private readonly ImmutableArray.Builder GetBuilder() var result = _pool.Get(); if (_capacity is int capacity) { - result.SetCapacityIfNeeded(capacity); + result.SetCapacityIfLarger(capacity); } return result; @@ -89,34 +89,11 @@ private readonly ImmutableArray.Builder GetBuilder() /// If equals , the /// internal array will be extracted as an without copying /// the contents. Otherwise, the contents will be copied into a new array. The collection - /// will then be set to a zero length array. + /// will then be set to a zero-length array. /// /// An immutable array. public readonly ImmutableArray DrainToImmutable() - { -#if NET8_0_OR_GREATER - return _builder?.DrainToImmutable() ?? ImmutableArray.Empty; -#else - if (_builder is not { } builder) - { - return ImmutableArray.Empty; - } - - if (builder.Count == 0) - { - return ImmutableArray.Empty; - } - - if (builder.Count == builder.Capacity) - { - return builder.MoveToImmutable(); - } - - var result = builder.ToImmutable(); - builder.Clear(); - return result; -#endif - } + => _builder?.DrainToImmutable() ?? ImmutableArray.Empty; public readonly ImmutableArray ToImmutable() => _builder?.ToImmutable() ?? ImmutableArray.Empty;