From 56128c3c26608077bb196ba6790ca44797c1fc2b Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 29 Aug 2024 17:42:10 +1000 Subject: [PATCH 1/7] Refactor out spell check endpoints to services, and clean up a little --- .../IServiceCollectionExtensions.cs | 3 + .../SpellCheck/DocumentSpellCheckEndpoint.cs | 187 ++---------------- .../SpellCheck/LspCSharpSpellCheckService.cs | 74 +++++++ .../SpellCheck/ICSharpSpellCheckService.cs | 14 ++ .../SpellCheck/ISpellCheckService.cs | 13 ++ .../SpellCheck/SpellCheckRange.cs | 6 + .../SpellCheck/SpellCheckService.cs | 137 +++++++++++++ .../DocumentSpellCheckEndpointTest.cs | 5 +- 8 files changed, 263 insertions(+), 176 deletions(-) create mode 100644 src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ISpellCheckService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckRange.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index 5b5a1911208..bd27fef9adc 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -27,6 +27,7 @@ using Microsoft.CodeAnalysis.Razor.ProjectSystem; using Microsoft.CodeAnalysis.Razor.Protocol; using Microsoft.CodeAnalysis.Razor.SemanticTokens; +using Microsoft.CodeAnalysis.Razor.SpellCheck; using Microsoft.CodeAnalysis.Razor.Workspaces; using Microsoft.CommonLanguageServerProtocol.Framework; using Microsoft.Extensions.DependencyInjection; @@ -159,6 +160,8 @@ public static void AddTextDocumentServices(this IServiceCollection services, Lan services.AddHandlerWithCapabilities(); } + services.AddSingleton(); + services.AddSingleton(); services.AddHandlerWithCapabilities(); services.AddHandler(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs index 625c7a6086b..5de64a1a7c9 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs @@ -2,38 +2,19 @@ // Licensed under the MIT license. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Razor.Language; -using Microsoft.AspNetCore.Razor.Language.Syntax; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; -using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; -using Microsoft.AspNetCore.Razor.PooledObjects; -using Microsoft.CodeAnalysis.Razor.DocumentMapping; -using Microsoft.CodeAnalysis.Razor.ProjectSystem; -using Microsoft.CodeAnalysis.Razor.Protocol; -using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.CodeAnalysis.Razor.SpellCheck; using Microsoft.VisualStudio.LanguageServer.Protocol; namespace Microsoft.AspNetCore.Razor.LanguageServer.SpellCheck; [RazorLanguageServerEndpoint(VSInternalMethods.TextDocumentSpellCheckableRangesName)] -internal sealed class DocumentSpellCheckEndpoint : IRazorRequestHandler, ICapabilitiesProvider +internal sealed class DocumentSpellCheckEndpoint( + ISpellCheckService spellCheckService) : IRazorRequestHandler, ICapabilitiesProvider { - private readonly IDocumentMappingService _documentMappingService; - private readonly LanguageServerFeatureOptions _languageServerFeatureOptions; - private readonly IClientConnection _clientConnection; - - public DocumentSpellCheckEndpoint( - IDocumentMappingService documentMappingService, - LanguageServerFeatureOptions languageServerFeatureOptions, - IClientConnection clientConnection) - { - _documentMappingService = documentMappingService ?? throw new ArgumentNullException(nameof(documentMappingService)); - _languageServerFeatureOptions = languageServerFeatureOptions ?? throw new ArgumentNullException(nameof(languageServerFeatureOptions)); - _clientConnection = clientConnection ?? throw new ArgumentNullException(nameof(clientConnection)); - } + private readonly ISpellCheckService _spellCheckService = spellCheckService; public bool MutatesSolutionState => false; @@ -43,14 +24,7 @@ public void ApplyCapabilities(VSInternalServerCapabilities serverCapabilities, V } public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentSpellCheckableParams request) - { - if (request.TextDocument is null) - { - throw new ArgumentNullException(nameof(request.TextDocument)); - } - - return request.TextDocument; - } + => request.TextDocument; public async Task HandleRequestAsync(VSInternalDocumentSpellCheckableParams request, RazorRequestContext requestContext, CancellationToken cancellationToken) { @@ -60,150 +34,13 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentSpellC return null; } - using var _ = ListPool.GetPooledObject(out var ranges); - - await AddRazorSpellCheckRangesAsync(ranges, documentContext, cancellationToken).ConfigureAwait(false); - - if (_languageServerFeatureOptions.SingleServerSupport) - { - await AddCSharpSpellCheckRangesAsync(ranges, documentContext, cancellationToken).ConfigureAwait(false); - } - - return new[] - { - new VSInternalSpellCheckableRangeReport - { - Ranges = ConvertSpellCheckRangesToIntTriples(ranges), - ResultId = Guid.NewGuid().ToString() - } - }; - } - - private static async Task AddRazorSpellCheckRangesAsync(List ranges, DocumentContext documentContext, CancellationToken cancellationToken) - { - var tree = await documentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - - // We don't want to report spelling errors in script or style tags, so we avoid descending into them at all, which - // means we don't need complicated logic, and it performs a bit better. We assume any C# in them will still be reported - // by Roslyn. - // In an ideal world we wouldn't need this logic at all, as we would defer to the Html LSP server to provide spell checking - // but it doesn't currently support it. When that support is added, we can remove all of this but the RazorCommentBlockSyntax - // handling. - foreach (var node in tree.Root.DescendantNodes(n => n is not MarkupElementSyntax { StartTag.Name.Content: "script" or "style" })) - { - if (node is RazorCommentBlockSyntax commentBlockSyntax) - { - ranges.Add(new((int)VSInternalSpellCheckableRangeKind.Comment, commentBlockSyntax.Comment.SpanStart, commentBlockSyntax.Comment.Span.Length)); - } - else if (node is MarkupTextLiteralSyntax textLiteralSyntax) - { - // Attribute names are text literals, but we don't want to spell check them because either C# will, - // whether they're component attributes based on property names, or they come from tag helper attribute - // parameters as strings, or they're Html attributes which are not necessarily expected to be real words. - if (node.Parent is MarkupTagHelperAttributeSyntax or - MarkupAttributeBlockSyntax or - MarkupMinimizedAttributeBlockSyntax or - MarkupTagHelperDirectiveAttributeSyntax or - MarkupMinimizedTagHelperAttributeSyntax or - MarkupMinimizedTagHelperDirectiveAttributeSyntax or - MarkupMiscAttributeContentSyntax) - { - continue; - } - - // Text literals appear everywhere in Razor to hold newlines and indentation, so its worth saving the tokens - if (textLiteralSyntax.ContainsOnlyWhitespace()) - { - continue; - } - - if (textLiteralSyntax.Span.Length == 0) - { - continue; - } - - ranges.Add(new((int)VSInternalSpellCheckableRangeKind.String, textLiteralSyntax.SpanStart, textLiteralSyntax.Span.Length)); - } - } - } - - private async Task AddCSharpSpellCheckRangesAsync(List ranges, DocumentContext documentContext, CancellationToken cancellationToken) - { - var delegatedParams = new DelegatedSpellCheckParams(documentContext.GetTextDocumentIdentifierAndVersion()); - var delegatedResponse = await _clientConnection.SendRequestAsync( - CustomMessageNames.RazorSpellCheckEndpoint, - delegatedParams, - cancellationToken).ConfigureAwait(false); + var data = await _spellCheckService.GetSpellCheckRangeTriplesAsync(documentContext, cancellationToken).ConfigureAwait(false); - if (delegatedResponse is null) - { - return; - } - - var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - var csharpDocument = codeDocument.GetCSharpDocument(); - - foreach (var report in delegatedResponse) - { - if (report.Ranges is not { } csharpRanges) - { - continue; - } - - // Since we get C# tokens that have relative starts, we need to convert them back to absolute indexes - // so we can sort them with the Razor tokens later - var absoluteCSharpStartIndex = 0; - for (var i = 0; i < csharpRanges.Length; i += 3) - { - var kind = csharpRanges[i]; - var start = csharpRanges[i + 1]; - var length = csharpRanges[i + 2]; - - absoluteCSharpStartIndex += start; - - // We need to map the start index to produce results, and we validate that we can map the end index so we don't have - // squiggles that go from C# into Razor/Html. - if (_documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex, out var _1, out var hostDocumentIndex) && - _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex + length, out var _2, out var _3)) - { - ranges.Add(new(kind, hostDocumentIndex, length)); - } - - absoluteCSharpStartIndex += length; - } - } - } - - private static int[] ConvertSpellCheckRangesToIntTriples(List ranges) - { - // Important to sort first, or the client will just ignore anything we say - ranges.Sort(CompareSpellCheckRanges); - - using var _ = ListPool.GetPooledObject(out var data); - data.SetCapacityIfLarger(ranges.Count * 3); - - var lastAbsoluteEndIndex = 0; - foreach (var range in ranges) - { - if (range.Length == 0) - { - continue; - } - - data.Add(range.Kind); - data.Add(range.AbsoluteStartIndex - lastAbsoluteEndIndex); - data.Add(range.Length); - - lastAbsoluteEndIndex = range.AbsoluteStartIndex + range.Length; - } - - return data.ToArray(); - } - - private record struct SpellCheckRange(int Kind, int AbsoluteStartIndex, int Length); - - private static int CompareSpellCheckRanges(SpellCheckRange x, SpellCheckRange y) - { - return x.AbsoluteStartIndex.CompareTo(y.AbsoluteStartIndex); + return [new VSInternalSpellCheckableRangeReport + { + Ranges =data, + ResultId = Guid.NewGuid().ToString() + } + ]; } } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckService.cs new file mode 100644 index 00000000000..764c9c1f31e --- /dev/null +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckService.cs @@ -0,0 +1,74 @@ +// 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 System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.LanguageServer.Hosting; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.Protocol; +using Microsoft.CodeAnalysis.Razor.SpellCheck; +using Microsoft.CodeAnalysis.Razor.Workspaces; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.AspNetCore.Razor.LanguageServer.SpellCheck; + +internal sealed class LspCSharpSpellCheckService( + LanguageServerFeatureOptions languageServerFeatureOptions, + IClientConnection clientConnection) : ICSharpSpellCheckService +{ + private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; + private readonly IClientConnection _clientConnection = clientConnection; + + public async Task> GetCSharpSpellCheckRangesAsync(DocumentContext documentContext, CancellationToken cancellationToken) + { + if (!_languageServerFeatureOptions.SingleServerSupport) + { + return []; + } + + var delegatedParams = new DelegatedSpellCheckParams(documentContext.GetTextDocumentIdentifierAndVersion()); + var delegatedResponse = await _clientConnection.SendRequestAsync( + CustomMessageNames.RazorSpellCheckEndpoint, + delegatedParams, + cancellationToken).ConfigureAwait(false); + + if (delegatedResponse is null) + { + return []; + } + + using var ranges = new PooledArrayBuilder(); + foreach (var report in delegatedResponse) + { + if (report.Ranges is not { } csharpRanges) + { + continue; + } + + // Since we get C# tokens that have relative starts, we need to convert them back to absolute indexes + // so we can sort them with the Razor tokens later + var absoluteCSharpStartIndex = 0; + for (var i = 0; i < csharpRanges.Length; i += 3) + { + var kind = csharpRanges[i]; + var start = csharpRanges[i + 1]; + var length = csharpRanges[i + 2]; + + absoluteCSharpStartIndex += start; + + ranges.Add(new(kind, absoluteCSharpStartIndex, length)); + + absoluteCSharpStartIndex += length; + } + } + + return ranges.ToImmutable(); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckService.cs new file mode 100644 index 00000000000..de7341567b6 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckService.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor.SpellCheck; + +internal interface ICSharpSpellCheckService +{ + Task> GetCSharpSpellCheckRangesAsync(DocumentContext documentContext, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ISpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ISpellCheckService.cs new file mode 100644 index 00000000000..dc45deee3b4 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ISpellCheckService.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Razor.SpellCheck; + +internal interface ISpellCheckService +{ + Task GetSpellCheckRangeTriplesAsync(DocumentContext documentContext, CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckRange.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckRange.cs new file mode 100644 index 00000000000..472d213f336 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckRange.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +namespace Microsoft.CodeAnalysis.Razor.SpellCheck; + +internal readonly record struct SpellCheckRange(int Kind, int AbsoluteStartIndex, int Length); diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs new file mode 100644 index 00000000000..78f0441e383 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs @@ -0,0 +1,137 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Syntax; +using Microsoft.AspNetCore.Razor.PooledObjects; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.Razor.SpellCheck; + +internal class SpellCheckService( + ICSharpSpellCheckService csharpSpellCheckService, + IDocumentMappingService documentMappingService) : ISpellCheckService +{ + private readonly ICSharpSpellCheckService _csharpSpellCheckService = csharpSpellCheckService; + private readonly IDocumentMappingService _documentMappingService = documentMappingService; + + public async Task GetSpellCheckRangeTriplesAsync(DocumentContext documentContext, CancellationToken cancellationToken) + { + using var ranges = new PooledArrayBuilder(); + + var syntaxTree = await documentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + + AddRazorSpellCheckRanges(ref ranges.AsRef(), syntaxTree); + + var csharpRanges = await _csharpSpellCheckService.GetCSharpSpellCheckRangesAsync(documentContext, cancellationToken).ConfigureAwait(false); + + if (csharpRanges.Length > 0) + { + var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); + AddCSharpSpellCheckRanges(ref ranges.AsRef(), csharpRanges, codeDocument); + } + + return ConvertSpellCheckRangesToIntTriples(ranges.ToImmutable()); + } + + private static void AddRazorSpellCheckRanges(ref PooledArrayBuilder ranges, RazorSyntaxTree syntaxTree) + { + // We don't want to report spelling errors in script or style tags, so we avoid descending into them at all, which + // means we don't need complicated logic, and it performs a bit better. We assume any C# in them will still be reported + // by Roslyn. + // In an ideal world we wouldn't need this logic at all, as we would defer to the Html LSP server to provide spell checking + // but it doesn't currently support it. When that support is added, we can remove all of this but the RazorCommentBlockSyntax + // handling. + foreach (var node in syntaxTree.Root.DescendantNodes(static n => n is not MarkupElementSyntax { StartTag.Name.Content: "script" or "style" })) + { + if (node is RazorCommentBlockSyntax commentBlockSyntax) + { + ranges.Add(new((int)VSInternalSpellCheckableRangeKind.Comment, commentBlockSyntax.Comment.SpanStart, commentBlockSyntax.Comment.Span.Length)); + } + else if (node is MarkupTextLiteralSyntax textLiteralSyntax) + { + // Attribute names are text literals, but we don't want to spell check them because either C# will, + // whether they're component attributes based on property names, or they come from tag helper attribute + // parameters as strings, or they're Html attributes which are not necessarily expected to be real words. + if (node.Parent is MarkupTagHelperAttributeSyntax or + MarkupAttributeBlockSyntax or + MarkupMinimizedAttributeBlockSyntax or + MarkupTagHelperDirectiveAttributeSyntax or + MarkupMinimizedTagHelperAttributeSyntax or + MarkupMinimizedTagHelperDirectiveAttributeSyntax or + MarkupMiscAttributeContentSyntax) + { + continue; + } + + // Text literals appear everywhere in Razor to hold newlines and indentation, so its worth saving the tokens + if (textLiteralSyntax.ContainsOnlyWhitespace()) + { + continue; + } + + if (textLiteralSyntax.Span.Length == 0) + { + continue; + } + + ranges.Add(new((int)VSInternalSpellCheckableRangeKind.String, textLiteralSyntax.SpanStart, textLiteralSyntax.Span.Length)); + } + } + } + + private void AddCSharpSpellCheckRanges(ref PooledArrayBuilder ranges, ImmutableArray csharpRanges, RazorCodeDocument codeDocument) + { + var csharpDocument = codeDocument.GetCSharpDocument(); + + foreach (var report in csharpRanges) + { + var absoluteCSharpStartIndex = report.AbsoluteStartIndex; + var length = report.Length; + + // We need to map the start index to produce results, and we validate that we can map the end index so we don't have + // squiggles that go from C# into Razor/Html. + if (_documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex, out var _1, out var hostDocumentIndex) && + _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex + length, out var _2, out var _3)) + { + ranges.Add(new(report.Kind, hostDocumentIndex, length)); + } + } + } + + private static int[] ConvertSpellCheckRangesToIntTriples(ImmutableArray ranges) + { + // Important to sort first as we're calculating relative indexes + ranges = ranges.OrderAsArray(CompareSpellCheckRanges); + + using var data = new PooledArrayBuilder(ranges.Length * 3); + + var lastAbsoluteEndIndex = 0; + foreach (var range in ranges) + { + if (range.Length == 0) + { + continue; + } + + data.Add(range.Kind); + data.Add(range.AbsoluteStartIndex - lastAbsoluteEndIndex); + data.Add(range.Length); + + lastAbsoluteEndIndex = range.AbsoluteStartIndex + range.Length; + } + + return data.ToArray(); + } + + private static int CompareSpellCheckRanges(SpellCheckRange x, SpellCheckRange y) + { + return x.AbsoluteStartIndex.CompareTo(y.AbsoluteStartIndex); + } +} diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SpellCheck/DocumentSpellCheckEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SpellCheck/DocumentSpellCheckEndpointTest.cs index 72f9c894e4f..5e547834463 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SpellCheck/DocumentSpellCheckEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SpellCheck/DocumentSpellCheckEndpointTest.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Razor.LanguageServer.EndpointContracts; +using Microsoft.CodeAnalysis.Razor.SpellCheck; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.LanguageServer.Protocol; @@ -174,7 +175,9 @@ private async Task ValidateSpellCheckRangesAsync(string originalInput, string? f var documentContext = CreateDocumentContext(uri, codeDocument); var requestContext = new RazorRequestContext(documentContext, null!, "lsp/method", uri: null); - var endpoint = new DocumentSpellCheckEndpoint(DocumentMappingService, LanguageServerFeatureOptions, languageServer); + var csharpSpellCheckService = new LspCSharpSpellCheckService(LanguageServerFeatureOptions, languageServer); + var spellCheckService = new SpellCheckService(csharpSpellCheckService, DocumentMappingService); + var endpoint = new DocumentSpellCheckEndpoint(spellCheckService); var request = new VSInternalDocumentSpellCheckableParams { From 4a44a99239697e3a5b9d5efcbf5a65ef029433da Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 3 Sep 2024 08:47:03 +1000 Subject: [PATCH 2/7] Create cohost spell check endpoints and services --- eng/targets/Services.props | 1 + .../IServiceCollectionExtensions.cs | 9 ++- .../SpellCheck/DocumentSpellCheckEndpoint.cs | 14 ++-- .../Remote/IRemoteSpellCheckService.cs | 16 ++++ .../Remote/RazorServices.cs | 1 + .../SpellCheck/OOPSpellCheckService.cs | 17 ++++ .../RemoteCSharpSpellCheckService.cs | 30 +++++++ .../SpellCheck/RemoteSpellCheckService.cs | 34 ++++++++ .../CohostDocumentSpellCheckEndpoint.cs | 80 +++++++++++++++++++ .../CohostWorkspaceSpellCheckEndpoint.cs | 34 ++++++++ 10 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteSpellCheckService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/OOPSpellCheckService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckService.cs create mode 100644 src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteSpellCheckService.cs create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs create mode 100644 src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWorkspaceSpellCheckEndpoint.cs diff --git a/eng/targets/Services.props b/eng/targets/Services.props index b2476eab94a..e2db181385e 100644 --- a/eng/targets/Services.props +++ b/eng/targets/Services.props @@ -29,5 +29,6 @@ + diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index bd27fef9adc..e4a7dc87af7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -158,12 +158,13 @@ public static void AddTextDocumentServices(this IServiceCollection services, Lan { services.AddHandlerWithCapabilities(); services.AddHandlerWithCapabilities(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddHandlerWithCapabilities(); + services.AddHandler(); } - services.AddSingleton(); - services.AddSingleton(); - services.AddHandlerWithCapabilities(); - services.AddHandler(); services.AddHandlerWithCapabilities(); services.AddHandler(); diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs index 5de64a1a7c9..aa2173d784d 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs @@ -36,11 +36,13 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentSpellC var data = await _spellCheckService.GetSpellCheckRangeTriplesAsync(documentContext, cancellationToken).ConfigureAwait(false); - return [new VSInternalSpellCheckableRangeReport - { - Ranges =data, - ResultId = Guid.NewGuid().ToString() - } - ]; + return + [ + new VSInternalSpellCheckableRangeReport + { + Ranges =data, + ResultId = Guid.NewGuid().ToString() + } + ]; } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteSpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteSpellCheckService.cs new file mode 100644 index 00000000000..f7233564653 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/IRemoteSpellCheckService.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; + +namespace Microsoft.CodeAnalysis.Razor.Remote; + +internal interface IRemoteSpellCheckService +{ + ValueTask GetSpellCheckRangeTriplesAsync( + RazorPinnedSolutionInfoWrapper solutionInfo, + DocumentId razorDocumentId, + CancellationToken cancellationToken); +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs index 558b737d4c4..a4e46afb63b 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs @@ -22,6 +22,7 @@ internal static class RazorServices (typeof(IRemoteFoldingRangeService), null), (typeof(IRemoteDocumentHighlightService), null), (typeof(IRemoteAutoInsertService), null), + (typeof(IRemoteSpellCheckService), null), ]; // Internal for testing diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/OOPSpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/OOPSpellCheckService.cs new file mode 100644 index 00000000000..f34e86b686d --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/OOPSpellCheckService.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using Microsoft.CodeAnalysis.Razor.DocumentMapping; +using Microsoft.CodeAnalysis.Razor.SpellCheck; + +namespace Microsoft.CodeAnalysis.Remote.Razor.SpellCheck; + +[Export(typeof(ISpellCheckService)), Shared] +[method: ImportingConstructor] +internal sealed class OOPSpellCheckService( + ICSharpSpellCheckService csharpSpellCheckService, + IDocumentMappingService documentMappingService) + : SpellCheckService(csharpSpellCheckService, documentMappingService) +{ +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckService.cs new file mode 100644 index 00000000000..d2779d745ab --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckService.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Composition; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Razor.ProjectSystem; +using Microsoft.CodeAnalysis.Razor.SpellCheck; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Remote.Razor.SpellCheck; + +[Export(typeof(ICSharpSpellCheckService)), Shared] +[method: ImportingConstructor] +internal class RemoteCSharpSpellCheckService() : ICSharpSpellCheckService +{ + public async Task> GetCSharpSpellCheckRangesAsync(DocumentContext documentContext, CancellationToken cancellationToken) + { + // We have a razor document, lets find the generated C# document + Debug.Assert(documentContext is RemoteDocumentContext, "This method only works on document snapshots created in the OOP process"); + var snapshot = (RemoteDocumentSnapshot)documentContext.Snapshot; + var generatedDocument = await snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false); + + var csharpRanges = await ExternalAccess.Razor.Cohost.Handlers.SpellCheck.GetSpellCheckSpansAsync(generatedDocument, cancellationToken).ConfigureAwait(false); + + return csharpRanges.SelectAsArray(r => new SpellCheckRange((int)r.Kind, r.StartIndex, r.Length)); + } +} diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteSpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteSpellCheckService.cs new file mode 100644 index 00000000000..161fbf7b5e3 --- /dev/null +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteSpellCheckService.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.Razor; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.CodeAnalysis.Razor.SpellCheck; +using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem; + +namespace Microsoft.CodeAnalysis.Remote.Razor; + +internal sealed partial class RemoteSpellCheckService(in ServiceArgs args) : RazorDocumentServiceBase(in args), IRemoteSpellCheckService +{ + internal sealed class Factory : FactoryBase + { + protected override IRemoteSpellCheckService CreateService(in ServiceArgs args) + => new RemoteSpellCheckService(in args); + } + + private readonly ISpellCheckService _spellCheckService = args.ExportProvider.GetExportedValue(); + + public ValueTask GetSpellCheckRangeTriplesAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, CancellationToken cancellationToken) + => RunServiceAsync( + solutionInfo, + razorDocumentId, + context => GetSpellCheckRangeTriplesAsync(context, cancellationToken), + cancellationToken); + + private async ValueTask GetSpellCheckRangeTriplesAsync(RemoteDocumentContext context, CancellationToken cancellationToken) + { + return await _spellCheckService.GetSpellCheckRangeTriplesAsync(context, cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs new file mode 100644 index 00000000000..9aa87a0cd69 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs @@ -0,0 +1,80 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(VSInternalMethods.TextDocumentSpellCheckableRangesName)] +[Export(typeof(IDynamicRegistrationProvider))] +[ExportCohostStatelessLspService(typeof(CohostDocumentSpellCheckEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal class CohostDocumentSpellCheckEndpoint( + IRemoteServiceInvoker remoteServiceInvoker) + : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => true; + + public Registration? GetRegistration(VSInternalClientCapabilities clientCapabilities, DocumentFilter[] filter, RazorCohostRequestContext requestContext) + { + if (clientCapabilities.SupportsVisualStudioExtensions) + { + return new Registration + { + Method = VSInternalMethods.TextDocumentSpellCheckableRangesName, + RegisterOptions = new TextDocumentRegistrationOptions() + { + DocumentSelector = filter + } + }; + } + + return null; + } + + protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSInternalDocumentSpellCheckableParams request) + => request.TextDocument.ToRazorTextDocumentIdentifier(); + + protected override Task HandleRequestAsync(VSInternalDocumentSpellCheckableParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => HandleRequestAsync(context.TextDocument.AssumeNotNull(), cancellationToken); + + private async Task HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken) + { + var data = await _remoteServiceInvoker.TryInvokeAsync( + razorDocument.Project.Solution, + (service, solutionInfo, cancellationToken) => service.GetSpellCheckRangeTriplesAsync(solutionInfo, razorDocument.Id, cancellationToken), + cancellationToken).ConfigureAwait(false); + + return + [ + new VSInternalSpellCheckableRangeReport + { + Ranges = data, + ResultId = Guid.NewGuid().ToString() + } + ]; + } + + internal TestAccessor GetTestAccessor() => new(this); + + internal readonly struct TestAccessor(CohostDocumentSpellCheckEndpoint instance) + { + public Task HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken) + => instance.HandleRequestAsync(razorDocument, cancellationToken); + } +} diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWorkspaceSpellCheckEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWorkspaceSpellCheckEndpoint.cs new file mode 100644 index 00000000000..cad02498732 --- /dev/null +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWorkspaceSpellCheckEndpoint.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor.Threading; +using Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; +using Microsoft.CodeAnalysis.Razor.Remote; +using Microsoft.VisualStudio.LanguageServer.Protocol; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +#pragma warning disable RS0030 // Do not use banned APIs +[Shared] +[CohostEndpoint(VSInternalMethods.WorkspaceSpellCheckableRangesName)] +[ExportCohostStatelessLspService(typeof(CohostWorkspaceSpellCheckEndpoint))] +[method: ImportingConstructor] +#pragma warning restore RS0030 // Do not use banned APIs +internal class CohostWorkspaceSpellCheckEndpoint( + IRemoteServiceInvoker remoteServiceInvoker) + : AbstractRazorCohostRequestHandler +{ + private readonly IRemoteServiceInvoker _remoteServiceInvoker = remoteServiceInvoker; + + protected override bool MutatesSolutionState => false; + + protected override bool RequiresLSPSolution => false; + + // Razor files generally don't do anything at the workspace level + + protected override Task HandleRequestAsync(VSInternalWorkspaceSpellCheckableParams request, RazorCohostRequestContext context, CancellationToken cancellationToken) + => SpecializedTasks.EmptyArray(); +} From 391f3227044b4de829c4fb900d06a6e79c2e5836 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Tue, 3 Sep 2024 09:31:12 +1000 Subject: [PATCH 3/7] Test --- .../TestCode.cs | 3 + .../CohostDocumentSpellCheckEndpointTest.cs | 100 ++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSpellCheckEndpointTest.cs diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestCode.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestCode.cs index d19cbc5281e..6930bda0d33 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestCode.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.Test.Common.Tooling/TestCode.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Common; internal readonly struct TestCode { + public string OriginalInput { get; } public string Text { get; } public ImmutableArray Positions { get; } @@ -17,6 +18,8 @@ internal readonly struct TestCode public TestCode(string input, bool treatPositionIndicatorsAsCode = false) { + OriginalInput = input; + if (treatPositionIndicatorsAsCode) { TestFileMarkupParser.GetSpans(input, treatPositionIndicatorsAsCode, out var text, out var nameToSpanMap); diff --git a/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSpellCheckEndpointTest.cs b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSpellCheckEndpointTest.cs new file mode 100644 index 00000000000..6711a4bdc2b --- /dev/null +++ b/src/Razor/test/Microsoft.VisualStudio.LanguageServices.Razor.Test/Cohost/CohostDocumentSpellCheckEndpointTest.cs @@ -0,0 +1,100 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Razor; +using Microsoft.AspNetCore.Razor.Test.Common; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Test.Utilities; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; + +public class CohostDocumentSpellCheckEndpointTest(ITestOutputHelper testOutputHelper) : CohostEndpointTestBase(testOutputHelper) +{ + [Fact] + public async Task Handle() + { + var input = """ + @page [|"this is csharp"|] + +
[| + + Eat more chickin. + + |]
+ + + + + + @{ var [|x|] = [|"csharp"|]; + + @*[| Eat more chickin. |]*@ + +
+ + @code + { + void [|M|]() + { + [|// Eat more chickin|] + } + } + """; + + await VerifySemanticTokensAsync(input); + } + + private async Task VerifySemanticTokensAsync(TestCode input) + { + var document = CreateProjectAndRazorDocument(input.Text); + var sourceText = await document.GetTextAsync(DisposalToken); + + var endpoint = new CohostDocumentSpellCheckEndpoint(RemoteServiceInvoker); + + var span = new LinePositionSpan(new(0, 0), new(sourceText.Lines.Count, 0)); + + var result = await endpoint.GetTestAccessor().HandleRequestAsync(document, DisposalToken); + + var ranges = result.First().Ranges.AssumeNotNull(); + + // To make for easier test failure analysis, we convert the ranges back to the test input, so we can show a diff + // rather than "Expected 23, got 53" and leave the developer to deal with what that means. + // As a bonus, this also ensures the ranges array has the right number of elements (ie, multiple of 3) + var absoluteRanges = new List<(int Start, int End)>(); + var absoluteStart = 0; + for (var i = 0; i < ranges.Length; i += 3) + { + var kind = ranges[i]; + var start = ranges[i + 1]; + var length = ranges[i + 2]; + + absoluteStart += start; + absoluteRanges.Add((absoluteStart, absoluteStart + length)); + absoluteStart += length; + } + + // Make sure the response is sorted correctly, or the IDE will complain + Assert.True(absoluteRanges.SequenceEqual(absoluteRanges.OrderBy(r => r.Start)), "Results are not in order!"); + + absoluteRanges.Reverse(); + + var actual = input.Text; + foreach (var (start, end) in absoluteRanges) + { + actual = actual.Insert(end, "|]").Insert(start, "[|"); + } + + AssertEx.EqualOrDiff(input.OriginalInput, actual); + } +} From d9e380b533723825b65b74ac5ca3157b1f5e251a Mon Sep 17 00:00:00 2001 From: David Wengier Date: Wed, 4 Sep 2024 11:17:50 +1000 Subject: [PATCH 4/7] Rename service --- .../Extensions/IServiceCollectionExtensions.cs | 2 +- ...ellCheckService.cs => LspCSharpSpellCheckRangeProvider.cs} | 4 ++-- ...SpellCheckService.cs => ICSharpSpellCheckRangeProvider.cs} | 2 +- .../SpellCheck/SpellCheckService.cs | 4 ++-- .../SpellCheck/OOPSpellCheckService.cs | 2 +- ...CheckService.cs => RemoteCSharpSpellCheckRangeProvider.cs} | 4 ++-- .../SpellCheck/DocumentSpellCheckEndpointTest.cs | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/{LspCSharpSpellCheckService.cs => LspCSharpSpellCheckRangeProvider.cs} (95%) rename src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/{ICSharpSpellCheckService.cs => ICSharpSpellCheckRangeProvider.cs} (90%) rename src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/{RemoteCSharpSpellCheckService.cs => RemoteCSharpSpellCheckRangeProvider.cs} (90%) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs index e4a7dc87af7..91ecc7a562f 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs @@ -160,7 +160,7 @@ public static void AddTextDocumentServices(this IServiceCollection services, Lan services.AddHandlerWithCapabilities(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); services.AddHandlerWithCapabilities(); services.AddHandler(); } diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckService.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckRangeProvider.cs similarity index 95% rename from src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckService.cs rename to src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckRangeProvider.cs index 764c9c1f31e..97cf038f3c7 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckService.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/LspCSharpSpellCheckRangeProvider.cs @@ -19,9 +19,9 @@ namespace Microsoft.AspNetCore.Razor.LanguageServer.SpellCheck; -internal sealed class LspCSharpSpellCheckService( +internal sealed class LspCSharpSpellCheckRangeProvider( LanguageServerFeatureOptions languageServerFeatureOptions, - IClientConnection clientConnection) : ICSharpSpellCheckService + IClientConnection clientConnection) : ICSharpSpellCheckRangeProvider { private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions; private readonly IClientConnection _clientConnection = clientConnection; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckRangeProvider.cs similarity index 90% rename from src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckService.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckRangeProvider.cs index de7341567b6..b25dc9f42dd 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/ICSharpSpellCheckRangeProvider.cs @@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis.Razor.SpellCheck; -internal interface ICSharpSpellCheckService +internal interface ICSharpSpellCheckRangeProvider { Task> GetCSharpSpellCheckRangesAsync(DocumentContext documentContext, CancellationToken cancellationToken); } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs index 78f0441e383..5690e39cff0 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs @@ -15,10 +15,10 @@ namespace Microsoft.CodeAnalysis.Razor.SpellCheck; internal class SpellCheckService( - ICSharpSpellCheckService csharpSpellCheckService, + ICSharpSpellCheckRangeProvider csharpSpellCheckService, IDocumentMappingService documentMappingService) : ISpellCheckService { - private readonly ICSharpSpellCheckService _csharpSpellCheckService = csharpSpellCheckService; + private readonly ICSharpSpellCheckRangeProvider _csharpSpellCheckService = csharpSpellCheckService; private readonly IDocumentMappingService _documentMappingService = documentMappingService; public async Task GetSpellCheckRangeTriplesAsync(DocumentContext documentContext, CancellationToken cancellationToken) diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/OOPSpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/OOPSpellCheckService.cs index f34e86b686d..1eecb538056 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/OOPSpellCheckService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/OOPSpellCheckService.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.SpellCheck; [Export(typeof(ISpellCheckService)), Shared] [method: ImportingConstructor] internal sealed class OOPSpellCheckService( - ICSharpSpellCheckService csharpSpellCheckService, + ICSharpSpellCheckRangeProvider csharpSpellCheckService, IDocumentMappingService documentMappingService) : SpellCheckService(csharpSpellCheckService, documentMappingService) { diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckRangeProvider.cs similarity index 90% rename from src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckService.cs rename to src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckRangeProvider.cs index d2779d745ab..970a1d51388 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SpellCheck/RemoteCSharpSpellCheckRangeProvider.cs @@ -12,9 +12,9 @@ namespace Microsoft.CodeAnalysis.Remote.Razor.SpellCheck; -[Export(typeof(ICSharpSpellCheckService)), Shared] +[Export(typeof(ICSharpSpellCheckRangeProvider)), Shared] [method: ImportingConstructor] -internal class RemoteCSharpSpellCheckService() : ICSharpSpellCheckService +internal class RemoteCSharpSpellCheckRangeProvider() : ICSharpSpellCheckRangeProvider { public async Task> GetCSharpSpellCheckRangesAsync(DocumentContext documentContext, CancellationToken cancellationToken) { diff --git a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SpellCheck/DocumentSpellCheckEndpointTest.cs b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SpellCheck/DocumentSpellCheckEndpointTest.cs index 5e547834463..a81ed10c7f1 100644 --- a/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SpellCheck/DocumentSpellCheckEndpointTest.cs +++ b/src/Razor/test/Microsoft.AspNetCore.Razor.LanguageServer.Test/SpellCheck/DocumentSpellCheckEndpointTest.cs @@ -175,7 +175,7 @@ private async Task ValidateSpellCheckRangesAsync(string originalInput, string? f var documentContext = CreateDocumentContext(uri, codeDocument); var requestContext = new RazorRequestContext(documentContext, null!, "lsp/method", uri: null); - var csharpSpellCheckService = new LspCSharpSpellCheckService(LanguageServerFeatureOptions, languageServer); + var csharpSpellCheckService = new LspCSharpSpellCheckRangeProvider(LanguageServerFeatureOptions, languageServer); var spellCheckService = new SpellCheckService(csharpSpellCheckService, DocumentMappingService); var endpoint = new DocumentSpellCheckEndpoint(spellCheckService); From e0b2935d30349080403d22379a13df2a512df713 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Thu, 5 Sep 2024 14:01:52 +1000 Subject: [PATCH 5/7] PR Feedback --- .../SpellCheck/DocumentSpellCheckEndpoint.cs | 2 +- .../Extensions/RazorSyntaxNodeExtensions.cs | 12 ++++++ .../SpellCheck/SpellCheckService.cs | 40 +++++++------------ .../RemoteCSharpSpellCheckRangeProvider.cs | 4 +- .../CohostDocumentSpellCheckEndpoint.cs | 2 +- .../CohostWorkspaceSpellCheckEndpoint.cs | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs index aa2173d784d..3cd304c989a 100644 --- a/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs +++ b/src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/SpellCheck/DocumentSpellCheckEndpoint.cs @@ -40,7 +40,7 @@ public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentSpellC [ new VSInternalSpellCheckableRangeReport { - Ranges =data, + Ranges = data, ResultId = Guid.NewGuid().ToString() } ]; diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxNodeExtensions.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxNodeExtensions.cs index e3232c19d44..a0d33a9155d 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxNodeExtensions.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Extensions/RazorSyntaxNodeExtensions.cs @@ -308,4 +308,16 @@ static bool IsCSharpCodeBlockSyntax(SyntaxNode node) return node is CSharpCodeBlockSyntax; } } + + public static bool IsAnyAttributeSyntax(this SyntaxNode node) + { + return node is + MarkupAttributeBlockSyntax or + MarkupMinimizedAttributeBlockSyntax or + MarkupTagHelperAttributeSyntax or + MarkupMinimizedTagHelperAttributeSyntax or + MarkupTagHelperDirectiveAttributeSyntax or + MarkupMinimizedTagHelperDirectiveAttributeSyntax or + MarkupMiscAttributeContentSyntax; + } } diff --git a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs index 5690e39cff0..0ab6c5ac690 100644 --- a/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs +++ b/src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/SpellCheck/SpellCheckService.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the MIT license. See License.txt in the project root for license information. -using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; @@ -23,21 +22,24 @@ internal class SpellCheckService( public async Task GetSpellCheckRangeTriplesAsync(DocumentContext documentContext, CancellationToken cancellationToken) { - using var ranges = new PooledArrayBuilder(); + using var builder = new PooledArrayBuilder(); var syntaxTree = await documentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - AddRazorSpellCheckRanges(ref ranges.AsRef(), syntaxTree); + AddRazorSpellCheckRanges(ref builder.AsRef(), syntaxTree); var csharpRanges = await _csharpSpellCheckService.GetCSharpSpellCheckRangesAsync(documentContext, cancellationToken).ConfigureAwait(false); if (csharpRanges.Length > 0) { var codeDocument = await documentContext.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false); - AddCSharpSpellCheckRanges(ref ranges.AsRef(), csharpRanges, codeDocument); + AddCSharpSpellCheckRanges(ref builder.AsRef(), csharpRanges, codeDocument); } - return ConvertSpellCheckRangesToIntTriples(ranges.ToImmutable()); + // Important to sort first as we're calculating relative indexes + var ranges = builder.ToImmutableOrderedBy(static r => r.AbsoluteStartIndex); + + return ConvertSpellCheckRangesToIntTriples(ranges); } private static void AddRazorSpellCheckRanges(ref PooledArrayBuilder ranges, RazorSyntaxTree syntaxTree) @@ -59,13 +61,7 @@ private static void AddRazorSpellCheckRanges(ref PooledArrayBuilder r { var csharpDocument = codeDocument.GetCSharpDocument(); - foreach (var report in csharpRanges) + foreach (var range in csharpRanges) { - var absoluteCSharpStartIndex = report.AbsoluteStartIndex; - var length = report.Length; + var absoluteCSharpStartIndex = range.AbsoluteStartIndex; + var length = range.Length; // We need to map the start index to produce results, and we validate that we can map the end index so we don't have // squiggles that go from C# into Razor/Html. - if (_documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex, out var _1, out var hostDocumentIndex) && - _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex + length, out var _2, out var _3)) + if (_documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex, out _, out var hostDocumentIndex) && + _documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex + length, out _, out _)) { - ranges.Add(new(report.Kind, hostDocumentIndex, length)); + ranges.Add(range with { AbsoluteStartIndex = hostDocumentIndex }); } } } private static int[] ConvertSpellCheckRangesToIntTriples(ImmutableArray ranges) { - // Important to sort first as we're calculating relative indexes - ranges = ranges.OrderAsArray(CompareSpellCheckRanges); - using var data = new PooledArrayBuilder(ranges.Length * 3); var lastAbsoluteEndIndex = 0; @@ -129,9 +122,4 @@ private static int[] ConvertSpellCheckRangesToIntTriples(ImmutableArray> GetCSharpSpellCheckRangesAsync(DocumentContext documentContext, CancellationToken cancellationToken) { @@ -25,6 +25,6 @@ public async Task> GetCSharpSpellCheckRangesAsyn var csharpRanges = await ExternalAccess.Razor.Cohost.Handlers.SpellCheck.GetSpellCheckSpansAsync(generatedDocument, cancellationToken).ConfigureAwait(false); - return csharpRanges.SelectAsArray(r => new SpellCheckRange((int)r.Kind, r.StartIndex, r.Length)); + return csharpRanges.SelectAsArray(static r => new SpellCheckRange((int)r.Kind, r.StartIndex, r.Length)); } } diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs index 9aa87a0cd69..e12e969f886 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostDocumentSpellCheckEndpoint.cs @@ -20,7 +20,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; [ExportCohostStatelessLspService(typeof(CohostDocumentSpellCheckEndpoint))] [method: ImportingConstructor] #pragma warning restore RS0030 // Do not use banned APIs -internal class CohostDocumentSpellCheckEndpoint( +internal sealed class CohostDocumentSpellCheckEndpoint( IRemoteServiceInvoker remoteServiceInvoker) : AbstractRazorCohostDocumentRequestHandler, IDynamicRegistrationProvider { diff --git a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWorkspaceSpellCheckEndpoint.cs b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWorkspaceSpellCheckEndpoint.cs index cad02498732..79876202a35 100644 --- a/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWorkspaceSpellCheckEndpoint.cs +++ b/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/CohostWorkspaceSpellCheckEndpoint.cs @@ -17,7 +17,7 @@ namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost; [ExportCohostStatelessLspService(typeof(CohostWorkspaceSpellCheckEndpoint))] [method: ImportingConstructor] #pragma warning restore RS0030 // Do not use banned APIs -internal class CohostWorkspaceSpellCheckEndpoint( +internal sealed class CohostWorkspaceSpellCheckEndpoint( IRemoteServiceInvoker remoteServiceInvoker) : AbstractRazorCohostRequestHandler { From d37da8b3de44606d1b1907483aff07640d324755 Mon Sep 17 00:00:00 2001 From: David Wengier Date: Fri, 6 Sep 2024 11:51:26 +1000 Subject: [PATCH 6/7] Bump to real Roslyn version --- eng/Version.Details.xml | 76 ++++++++++++++++++++--------------------- eng/Versions.props | 38 ++++++++++----------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index a879142cf4a..b9aa7509484 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -11,82 +11,82 @@ 6bcf90f99d13da86c5e9753a6f34b6484673d0a0 - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb - + https://github.com/dotnet/roslyn - 0ec44d9775c80a6861b811d8af637ee36e3315e1 + 9f86520c46f67d2a8a59af189f8fd87e35c574bb diff --git a/eng/Versions.props b/eng/Versions.props index 5df1d73efbd..b17446f0174 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -53,25 +53,25 @@ 9.0.0-beta.24453.1 1.0.0-beta.23475.1 1.0.0-beta.23475.1 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 - 4.12.0-2.24419.3 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5 + 4.12.0-3.24454.5