Merge remote-tracking branch 'upstream/main' into MinFix

This commit is contained in:
David Wengier 2024-09-06 17:08:07 +10:00
Родитель daa1f0183c 07e138222c
Коммит 9386c8c975
20 изменённых файлов: 583 добавлений и 176 удалений

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

@ -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;