Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cohost Spell Check #10825

Merged
merged 9 commits into from
Sep 6, 2024
Merged
1 change: 1 addition & 0 deletions eng/targets/Services.props
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
<ServiceHubService Include="Microsoft.VisualStudio.Razor.GoToDefinition" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteGoToDefinitionService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.Rename" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteRenameService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.AutoInsert" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteAutoInsertService+Factory" />
<ServiceHubService Include="Microsoft.VisualStudio.Razor.SpellCheck" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteSpellCheckService+Factory" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,13 @@ public static void AddTextDocumentServices(this IServiceCollection services, Lan
{
services.AddHandlerWithCapabilities<TextDocumentTextPresentationEndpoint>();
services.AddHandlerWithCapabilities<TextDocumentUriPresentationEndpoint>();

services.AddSingleton<ISpellCheckService, SpellCheckService>();
services.AddSingleton<ICSharpSpellCheckService, LspCSharpSpellCheckService>();
services.AddHandlerWithCapabilities<DocumentSpellCheckEndpoint>();
services.AddHandler<WorkspaceSpellCheckEndpoint>();
}

services.AddSingleton<ISpellCheckService, SpellCheckService>();
services.AddSingleton<ICSharpSpellCheckService, LspCSharpSpellCheckService>();
services.AddHandlerWithCapabilities<DocumentSpellCheckEndpoint>();
services.AddHandler<WorkspaceSpellCheckEndpoint>();

davidwengier marked this conversation as resolved.
Show resolved Hide resolved
services.AddHandlerWithCapabilities<DocumentDidChangeEndpoint>();
services.AddHandler<DocumentDidCloseEndpoint>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
ResultId = Guid.NewGuid().ToString()
}
];
}
}
Original file line number Diff line number Diff line change
@@ -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(
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
RazorPinnedSolutionInfoWrapper solutionInfo,
DocumentId razorDocumentId,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal static class RazorServices
(typeof(IRemoteFoldingRangeService), null),
(typeof(IRemoteDocumentHighlightService), null),
(typeof(IRemoteAutoInsertService), null),
(typeof(IRemoteSpellCheckService), null),
];

// Internal for testing
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
{
}
Original file line number Diff line number Diff line change
@@ -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<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(r => new SpellCheckRange((int)r.Kind, r.StartIndex, r.Length));
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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(
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
Original file line number Diff line number Diff line change
@@ -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(
davidwengier marked this conversation as resolved.
Show resolved Hide resolved
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>();
}