зеркало из https://github.com/dotnet/razor.git
Merge remote-tracking branch 'upstream/main' into MinFix
This commit is contained in:
Коммит
9386c8c975
|
@ -31,5 +31,6 @@
|
|||
<ServiceHubService Include="Microsoft.VisualStudio.Razor.AutoInsert" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteAutoInsertService+Factory" />
|
||||
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Formatting" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteFormattingService+Factory" />
|
||||
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToImplementation" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToImplementationService+Factory" />
|
||||
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SpellCheck" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSpellCheckService+Factory" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -27,6 +27,7 @@ using Microsoft.CodeAnalysis.Razor.Formatting;
|
|||
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;
|
||||
|
@ -161,10 +162,12 @@ internal static class IServiceCollectionExtensions
|
|||
{
|
||||
services.AddHandlerWithCapabilities<TextDocumentTextPresentationEndpoint>();
|
||||
services.AddHandlerWithCapabilities<TextDocumentUriPresentationEndpoint>();
|
||||
}
|
||||
|
||||
services.AddHandlerWithCapabilities<DocumentSpellCheckEndpoint>();
|
||||
services.AddHandler<WorkspaceSpellCheckEndpoint>();
|
||||
services.AddSingleton<ISpellCheckService, SpellCheckService>();
|
||||
services.AddSingleton<ICSharpSpellCheckRangeProvider, LspCSharpSpellCheckRangeProvider>();
|
||||
services.AddHandlerWithCapabilities<DocumentSpellCheckEndpoint>();
|
||||
services.AddHandler<WorkspaceSpellCheckEndpoint>();
|
||||
}
|
||||
|
||||
services.AddHandlerWithCapabilities<DocumentDidChangeEndpoint>();
|
||||
services.AddHandler<DocumentDidCloseEndpoint>();
|
||||
|
|
|
@ -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<VSInternalDocumentSpellCheckableParams, VSInternalSpellCheckableRangeReport[]?>, ICapabilitiesProvider
|
||||
internal sealed class DocumentSpellCheckEndpoint(
|
||||
ISpellCheckService spellCheckService) : IRazorRequestHandler<VSInternalDocumentSpellCheckableParams, VSInternalSpellCheckableRangeReport[]?>, 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 @@ internal sealed class DocumentSpellCheckEndpoint : IRazorRequestHandler<VSIntern
|
|||
}
|
||||
|
||||
public TextDocumentIdentifier GetTextDocumentIdentifier(VSInternalDocumentSpellCheckableParams request)
|
||||
{
|
||||
if (request.TextDocument is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request.TextDocument));
|
||||
}
|
||||
|
||||
return request.TextDocument;
|
||||
}
|
||||
=> request.TextDocument;
|
||||
|
||||
public async Task<VSInternalSpellCheckableRangeReport[]?> HandleRequestAsync(VSInternalDocumentSpellCheckableParams request, RazorRequestContext requestContext, CancellationToken cancellationToken)
|
||||
{
|
||||
|
@ -60,150 +34,15 @@ internal sealed class DocumentSpellCheckEndpoint : IRazorRequestHandler<VSIntern
|
|||
return null;
|
||||
}
|
||||
|
||||
using var _ = ListPool<SpellCheckRange>.GetPooledObject(out var ranges);
|
||||
var data = await _spellCheckService.GetSpellCheckRangeTriplesAsync(documentContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await AddRazorSpellCheckRangesAsync(ranges, documentContext, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (_languageServerFeatureOptions.SingleServerSupport)
|
||||
{
|
||||
await AddCSharpSpellCheckRangesAsync(ranges, documentContext, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new[]
|
||||
{
|
||||
return
|
||||
[
|
||||
new VSInternalSpellCheckableRangeReport
|
||||
{
|
||||
Ranges = ConvertSpellCheckRangesToIntTriples(ranges),
|
||||
Ranges = data,
|
||||
ResultId = Guid.NewGuid().ToString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task AddRazorSpellCheckRangesAsync(List<SpellCheckRange> 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<SpellCheckRange> ranges, DocumentContext documentContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var delegatedParams = new DelegatedSpellCheckParams(documentContext.GetTextDocumentIdentifierAndVersion());
|
||||
var delegatedResponse = await _clientConnection.SendRequestAsync<DelegatedSpellCheckParams, VSInternalSpellCheckableRangeReport[]?>(
|
||||
CustomMessageNames.RazorSpellCheckEndpoint,
|
||||
delegatedParams,
|
||||
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<SpellCheckRange> ranges)
|
||||
{
|
||||
// Important to sort first, or the client will just ignore anything we say
|
||||
ranges.Sort(CompareSpellCheckRanges);
|
||||
|
||||
using var _ = ListPool<int>.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);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// 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 LspCSharpSpellCheckRangeProvider(
|
||||
LanguageServerFeatureOptions languageServerFeatureOptions,
|
||||
IClientConnection clientConnection) : ICSharpSpellCheckRangeProvider
|
||||
{
|
||||
private readonly LanguageServerFeatureOptions _languageServerFeatureOptions = languageServerFeatureOptions;
|
||||
private readonly IClientConnection _clientConnection = clientConnection;
|
||||
|
||||
public async Task<ImmutableArray<SpellCheckRange>> GetCSharpSpellCheckRangesAsync(DocumentContext documentContext, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!_languageServerFeatureOptions.SingleServerSupport)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var delegatedParams = new DelegatedSpellCheckParams(documentContext.GetTextDocumentIdentifierAndVersion());
|
||||
var delegatedResponse = await _clientConnection.SendRequestAsync<DelegatedSpellCheckParams, VSInternalSpellCheckableRangeReport[]?>(
|
||||
CustomMessageNames.RazorSpellCheckEndpoint,
|
||||
delegatedParams,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (delegatedResponse is not [_, ..] response)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Most common case is we'll get one report back from Roslyn, so we'll use that as the initial capacity.
|
||||
var initialCapacity = response[0].Ranges?.Length ?? 4;
|
||||
|
||||
using var ranges = new PooledArrayBuilder<SpellCheckRange>(initialCapacity);
|
||||
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.DrainToImmutable();
|
||||
}
|
||||
}
|
|
@ -308,4 +308,16 @@ internal static class RazorSyntaxNodeExtensions
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<int[]> GetSpellCheckRangeTriplesAsync(
|
||||
RazorPinnedSolutionInfoWrapper solutionInfo,
|
||||
DocumentId razorDocumentId,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
|
@ -23,6 +23,7 @@ internal static class RazorServices
|
|||
(typeof(IRemoteDocumentHighlightService), null),
|
||||
(typeof(IRemoteAutoInsertService), null),
|
||||
(typeof(IRemoteFormattingService), null),
|
||||
(typeof(IRemoteSpellCheckService), null),
|
||||
];
|
||||
|
||||
// Internal for testing
|
||||
|
|
|
@ -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 ICSharpSpellCheckRangeProvider
|
||||
{
|
||||
Task<ImmutableArray<SpellCheckRange>> GetCSharpSpellCheckRangesAsync(DocumentContext documentContext, CancellationToken cancellationToken);
|
||||
}
|
|
@ -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<int[]> GetSpellCheckRangeTriplesAsync(DocumentContext documentContext, CancellationToken cancellationToken);
|
||||
}
|
|
@ -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);
|
|
@ -0,0 +1,125 @@
|
|||
// 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.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(
|
||||
ICSharpSpellCheckRangeProvider csharpSpellCheckService,
|
||||
IDocumentMappingService documentMappingService) : ISpellCheckService
|
||||
{
|
||||
private readonly ICSharpSpellCheckRangeProvider _csharpSpellCheckService = csharpSpellCheckService;
|
||||
private readonly IDocumentMappingService _documentMappingService = documentMappingService;
|
||||
|
||||
public async Task<int[]> GetSpellCheckRangeTriplesAsync(DocumentContext documentContext, CancellationToken cancellationToken)
|
||||
{
|
||||
using var builder = new PooledArrayBuilder<SpellCheckRange>();
|
||||
|
||||
var syntaxTree = await documentContext.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
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 builder.AsRef(), csharpRanges, codeDocument);
|
||||
}
|
||||
|
||||
// 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<SpellCheckRange> 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.IsAnyAttributeSyntax())
|
||||
{
|
||||
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<SpellCheckRange> ranges, ImmutableArray<SpellCheckRange> csharpRanges, RazorCodeDocument codeDocument)
|
||||
{
|
||||
var csharpDocument = codeDocument.GetCSharpDocument();
|
||||
|
||||
foreach (var range in csharpRanges)
|
||||
{
|
||||
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 _, out var hostDocumentIndex) &&
|
||||
_documentMappingService.TryMapToHostDocumentPosition(csharpDocument, absoluteCSharpStartIndex + length, out _, out _))
|
||||
{
|
||||
ranges.Add(range with { AbsoluteStartIndex = hostDocumentIndex });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] ConvertSpellCheckRangesToIntTriples(ImmutableArray<SpellCheckRange> ranges)
|
||||
{
|
||||
using var data = new PooledArrayBuilder<int>(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();
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
ICSharpSpellCheckRangeProvider csharpSpellCheckService,
|
||||
IDocumentMappingService documentMappingService)
|
||||
: SpellCheckService(csharpSpellCheckService, documentMappingService)
|
||||
{
|
||||
}
|
|
@ -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(ICSharpSpellCheckRangeProvider)), Shared]
|
||||
[method: ImportingConstructor]
|
||||
internal sealed class RemoteCSharpSpellCheckRangeProvider() : ICSharpSpellCheckRangeProvider
|
||||
{
|
||||
public async Task<ImmutableArray<SpellCheckRange>> 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(static r => new SpellCheckRange((int)r.Kind, r.StartIndex, r.Length));
|
||||
}
|
||||
}
|
|
@ -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<IRemoteSpellCheckService>
|
||||
{
|
||||
protected override IRemoteSpellCheckService CreateService(in ServiceArgs args)
|
||||
=> new RemoteSpellCheckService(in args);
|
||||
}
|
||||
|
||||
private readonly ISpellCheckService _spellCheckService = args.ExportProvider.GetExportedValue<ISpellCheckService>();
|
||||
|
||||
public ValueTask<int[]> GetSpellCheckRangeTriplesAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, CancellationToken cancellationToken)
|
||||
=> RunServiceAsync(
|
||||
solutionInfo,
|
||||
razorDocumentId,
|
||||
context => GetSpellCheckRangeTriplesAsync(context, cancellationToken),
|
||||
cancellationToken);
|
||||
|
||||
private async ValueTask<int[]> GetSpellCheckRangeTriplesAsync(RemoteDocumentContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _spellCheckService.GetSpellCheckRangeTriplesAsync(context, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
|
@ -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 sealed class CohostDocumentSpellCheckEndpoint(
|
||||
IRemoteServiceInvoker remoteServiceInvoker)
|
||||
: AbstractRazorCohostDocumentRequestHandler<VSInternalDocumentSpellCheckableParams, VSInternalSpellCheckableRangeReport[]>, 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<VSInternalSpellCheckableRangeReport[]> HandleRequestAsync(VSInternalDocumentSpellCheckableParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
|
||||
=> HandleRequestAsync(context.TextDocument.AssumeNotNull(), cancellationToken);
|
||||
|
||||
private async Task<VSInternalSpellCheckableRangeReport[]> HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
{
|
||||
var data = await _remoteServiceInvoker.TryInvokeAsync<IRemoteSpellCheckService, int[]>(
|
||||
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<VSInternalSpellCheckableRangeReport[]> HandleRequestAsync(TextDocument razorDocument, CancellationToken cancellationToken)
|
||||
=> instance.HandleRequestAsync(razorDocument, cancellationToken);
|
||||
}
|
||||
}
|
|
@ -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 sealed class CohostWorkspaceSpellCheckEndpoint(
|
||||
IRemoteServiceInvoker remoteServiceInvoker)
|
||||
: AbstractRazorCohostRequestHandler<VSInternalWorkspaceSpellCheckableParams, VSInternalWorkspaceSpellCheckableReport[]>
|
||||
{
|
||||
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<VSInternalWorkspaceSpellCheckableReport[]> HandleRequestAsync(VSInternalWorkspaceSpellCheckableParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
|
||||
=> SpecializedTasks.EmptyArray<VSInternalWorkspaceSpellCheckableReport>();
|
||||
}
|
|
@ -7,6 +7,7 @@ using System.Collections.Immutable;
|
|||
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 @@ public class DocumentSpellCheckEndpointTest(ITestOutputHelper testOutput) : Sing
|
|||
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 LspCSharpSpellCheckRangeProvider(LanguageServerFeatureOptions, languageServer);
|
||||
var spellCheckService = new SpellCheckService(csharpSpellCheckService, DocumentMappingService);
|
||||
var endpoint = new DocumentSpellCheckEndpoint(spellCheckService);
|
||||
|
||||
var request = new VSInternalDocumentSpellCheckableParams
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Razor.Test.Common;
|
|||
|
||||
internal readonly struct TestCode
|
||||
{
|
||||
public string OriginalInput { get; }
|
||||
public string Text { get; }
|
||||
public ImmutableArray<int> 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);
|
||||
|
|
|
@ -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"|]
|
||||
|
||||
<div>[|
|
||||
|
||||
Eat more chickin.
|
||||
|
||||
|]</div>
|
||||
|
||||
<script>
|
||||
// no spell checking of script tags
|
||||
@([|"unless they contain csharp"|])
|
||||
</script>
|
||||
|
||||
<style>
|
||||
// no spell checking of style tags
|
||||
@([|"unless they contain csharp"|])
|
||||
</style>
|
||||
|
||||
@{ var [|x|] = [|"csharp"|];
|
||||
|
||||
@*[| Eat more chickin. |]*@
|
||||
|
||||
<div class="[|fush|]" />
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ using Microsoft.CodeAnalysis.Razor.Formatting;
|
|||
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
|
||||
using Microsoft.VisualStudio.LanguageServer.Protocol;
|
||||
using Microsoft.VisualStudio.Razor.Settings;
|
||||
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
|
||||
using Microsoft.VisualStudio.Threading;
|
||||
using Roslyn.Test.Utilities;
|
||||
using Xunit;
|
||||
|
|
Загрузка…
Ссылка в новой задаче