Skip to content

Commit

Permalink
Cohosting tests for Uri presentation (#10642)
Browse files Browse the repository at this point in the history
Part of #9519 and
#10603

After this, all cohost endpoints have test coverage 😁
  • Loading branch information
davidwengier authored Jul 19, 2024
2 parents c79c968 + 3f524c0 commit a12e07b
Show file tree
Hide file tree
Showing 6 changed files with 390 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Runtime.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
Expand All @@ -11,5 +12,10 @@ namespace Microsoft.CodeAnalysis.Razor.Remote;

internal interface IRemoteUriPresentationService
{
ValueTask<TextChange?> GetPresentationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken);
ValueTask<Response> GetPresentationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken);

[DataContract]
public record struct Response(
[property: DataMember(Order = 0)] bool CallHtml,
[property: DataMember(Order = 1)] TextChange? TextChange);
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar

private readonly IRazorDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IRazorDocumentMappingService>();

public ValueTask<TextChange?> GetPresentationAsync(
public ValueTask<IRemoteUriPresentationService.Response> GetPresentationAsync(
RazorPinnedSolutionInfoWrapper solutionInfo,
DocumentId razorDocumentId,
LinePositionSpan span,
Expand All @@ -37,7 +37,11 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar
context => GetPresentationAsync(context, span, uris, cancellationToken),
cancellationToken);

private async ValueTask<TextChange?> GetPresentationAsync(
private static IRemoteUriPresentationService.Response CallHtml => new(CallHtml: true, TextChange: null);
private static IRemoteUriPresentationService.Response NoFurtherHandling => new(CallHtml: false, TextChange: null);
private static IRemoteUriPresentationService.Response TextChange(TextChange textChange) => new(CallHtml: false, TextChange: textChange);

private async ValueTask<IRemoteUriPresentationService.Response> GetPresentationAsync(
RemoteDocumentContext context,
LinePositionSpan span,
Uri[]? uris,
Expand All @@ -46,7 +50,8 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar
var sourceText = await context.GetSourceTextAsync(cancellationToken).ConfigureAwait(false);
if (!sourceText.TryGetAbsoluteIndex(span.Start.Line, span.Start.Character, out var index))
{
return null;
// If the position is invalid then we shouldn't expect to be able to handle a Html response
return NoFurtherHandling;
}

var codeDocument = await context.GetCodeDocumentAsync(cancellationToken).ConfigureAwait(false);
Expand All @@ -58,13 +63,13 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar
// our support for Uri presentation is to insert a Html tag, so we only support Html

// If Roslyn add support in future then this is where it would go.
return null;
return NoFurtherHandling;
}

var razorFileUri = UriPresentationHelper.GetComponentFileNameFromUriPresentationRequest(uris, Logger);
if (razorFileUri is null)
{
return null;
return CallHtml;
}

var solution = context.TextDocument.Project.Solution;
Expand All @@ -74,30 +79,30 @@ protected override IRemoteUriPresentationService CreateService(in ServiceArgs ar
var ids = solution.GetDocumentIdsWithFilePath(uriToFind);
if (ids.Length == 0)
{
return null;
return CallHtml;
}

// We assume linked documents would produce the same component tag so just take the first
var otherDocument = solution.GetAdditionalDocument(ids[0]);
if (otherDocument is null)
{
return null;
return CallHtml;
}

var otherSnapshot = DocumentSnapshotFactory.GetOrCreate(otherDocument);
var descriptor = await otherSnapshot.TryGetTagHelperDescriptorAsync(cancellationToken).ConfigureAwait(false);

if (descriptor is null)
{
return null;
return CallHtml;
}

var tag = descriptor.TryGetComponentTag();
if (tag is null)
{
return null;
return CallHtml;
}

return new TextChange(span.ToTextSpan(sourceText), tag);
return TextChange(new TextChange(span.ToTextSpan(sourceText), tag));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
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.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.LanguageServer.ContainedLanguage;
using Microsoft.VisualStudio.LanguageServer.Protocol;
using Microsoft.VisualStudio.Razor.LanguageClient.Extensions;
Expand Down Expand Up @@ -58,37 +58,43 @@ internal class CohostUriPresentationEndpoint(
protected override RazorTextDocumentIdentifier? GetRazorTextDocumentIdentifier(VSInternalUriPresentationParams request)
=> request.TextDocument.ToRazorTextDocumentIdentifier();

protected override async Task<WorkspaceEdit?> HandleRequestAsync(VSInternalUriPresentationParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
{
var razorDocument = context.TextDocument.AssumeNotNull();
protected override Task<WorkspaceEdit?> HandleRequestAsync(VSInternalUriPresentationParams request, RazorCohostRequestContext context, CancellationToken cancellationToken)
=> HandleRequestAsync(request, context.TextDocument.AssumeNotNull(), cancellationToken);

var data = await _remoteServiceInvoker.TryInvokeAsync<IRemoteUriPresentationService, TextChange?>(
razorDocument.Project.Solution,
(service, solutionInfo, cancellationToken) => service.GetPresentationAsync(solutionInfo, razorDocument.Id, request.Range.ToLinePositionSpan(), request.Uris, cancellationToken),
cancellationToken).ConfigureAwait(false);
private async Task<WorkspaceEdit?> HandleRequestAsync(VSInternalUriPresentationParams request, TextDocument razorDocument, CancellationToken cancellationToken)
{
var data = await _remoteServiceInvoker.TryInvokeAsync<IRemoteUriPresentationService, IRemoteUriPresentationService.Response>(
razorDocument.Project.Solution,
(service, solutionInfo, cancellationToken) => service.GetPresentationAsync(solutionInfo, razorDocument.Id, request.Range.ToLinePositionSpan(), request.Uris, cancellationToken),
cancellationToken).ConfigureAwait(false);

// If we got a response back, then either Razor or C# wants to do something with this, so we're good to go
if (data is { } textChange)
// If we got a response back, then we're good to go
if (data.TextChange is { } textChange)
{
var sourceText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);

return new WorkspaceEdit
{
DocumentChanges = new TextDocumentEdit[]
{
new TextDocumentEdit
new TextDocumentEdit
{
TextDocument = new()
{
TextDocument = new()
{
Uri = request.TextDocument.Uri
},
Edits = [textChange.ToTextEdit(sourceText)]
}
Uri = request.TextDocument.Uri
},
Edits = [textChange.ToTextEdit(sourceText)]
}
}
};
}

// If we didn't get anything from Razor or Roslyn, lets ask Html what they want to do
// If we didn't get anything from our logic, we might need to go and ask Html, but we also might have determined not to
if (!data.CallHtml)
{
return null;
}

var htmlDocument = await _htmlDocumentSynchronizer.TryGetSynchronizedHtmlDocumentAsync(razorDocument, cancellationToken).ConfigureAwait(false);
if (htmlDocument is null)
{
Expand Down Expand Up @@ -134,4 +140,12 @@ internal class CohostUriPresentationEndpoint(

return workspaceEdit;
}

internal TestAccessor GetTestAccessor() => new(this);

internal readonly struct TestAccessor(CohostUriPresentationEndpoint instance)
{
public Task<WorkspaceEdit?> HandleRequestAsync(VSInternalUriPresentationParams request, TextDocument razorDocument, CancellationToken cancellationToken)
=> instance.HandleRequestAsync(request, razorDocument, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@ internal static class TestProjectData
static TestProjectData()
{
var baseDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "c:\\users\\example\\src" : "/home/example";
var someProjectPath = Path.Combine(baseDirectory, "SomeProject");
var someProjectObjPath = Path.Combine(someProjectPath, "obj");
SomeProjectPath = Path.Combine(baseDirectory, "SomeProject");
var someProjectObjPath = Path.Combine(SomeProjectPath, "obj");

SomeProject = new HostProject(Path.Combine(someProjectPath, "SomeProject.csproj"), someProjectObjPath, RazorConfiguration.Default, "SomeProject");
SomeProjectFile1 = new HostDocument(Path.Combine(someProjectPath, "File1.cshtml"), "File1.cshtml", FileKinds.Legacy);
SomeProjectFile2 = new HostDocument(Path.Combine(someProjectPath, "File2.cshtml"), "File2.cshtml", FileKinds.Legacy);
SomeProjectImportFile = new HostDocument(Path.Combine(someProjectPath, "_ViewImports.cshtml"), "_ViewImports.cshtml", FileKinds.Legacy);
SomeProjectNestedFile3 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File3.cshtml"), "Nested\\File3.cshtml", FileKinds.Legacy);
SomeProjectNestedFile4 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File4.cshtml"), "Nested\\File4.cshtml", FileKinds.Legacy);
SomeProjectNestedImportFile = new HostDocument(Path.Combine(someProjectPath, "Nested", "_ViewImports.cshtml"), "Nested\\_ViewImports.cshtml", FileKinds.Legacy);
SomeProjectComponentFile1 = new HostDocument(Path.Combine(someProjectPath, "File1.razor"), "File1.razor", FileKinds.Component);
SomeProjectComponentFile2 = new HostDocument(Path.Combine(someProjectPath, "File2.razor"), "File2.razor", FileKinds.Component);
SomeProjectComponentImportFile1 = new HostDocument(Path.Combine(someProjectPath, "_Imports.razor"), "_Imports.razor", FileKinds.Component);
SomeProjectNestedComponentFile3 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File3.razor"), "Nested\\File3.razor", FileKinds.Component);
SomeProjectNestedComponentFile4 = new HostDocument(Path.Combine(someProjectPath, "Nested", "File4.razor"), "Nested\\File4.razor", FileKinds.Component);
SomeProjectCshtmlComponentFile5 = new HostDocument(Path.Combine(someProjectPath, "File5.cshtml"), "File5.cshtml", FileKinds.Component);
SomeProject = new HostProject(Path.Combine(SomeProjectPath, "SomeProject.csproj"), someProjectObjPath, RazorConfiguration.Default, "SomeProject");
SomeProjectFile1 = new HostDocument(Path.Combine(SomeProjectPath, "File1.cshtml"), "File1.cshtml", FileKinds.Legacy);
SomeProjectFile2 = new HostDocument(Path.Combine(SomeProjectPath, "File2.cshtml"), "File2.cshtml", FileKinds.Legacy);
SomeProjectImportFile = new HostDocument(Path.Combine(SomeProjectPath, "_ViewImports.cshtml"), "_ViewImports.cshtml", FileKinds.Legacy);
SomeProjectNestedFile3 = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "File3.cshtml"), "Nested\\File3.cshtml", FileKinds.Legacy);
SomeProjectNestedFile4 = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "File4.cshtml"), "Nested\\File4.cshtml", FileKinds.Legacy);
SomeProjectNestedImportFile = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "_ViewImports.cshtml"), "Nested\\_ViewImports.cshtml", FileKinds.Legacy);
SomeProjectComponentFile1 = new HostDocument(Path.Combine(SomeProjectPath, "File1.razor"), "File1.razor", FileKinds.Component);
SomeProjectComponentFile2 = new HostDocument(Path.Combine(SomeProjectPath, "File2.razor"), "File2.razor", FileKinds.Component);
SomeProjectComponentImportFile1 = new HostDocument(Path.Combine(SomeProjectPath, "_Imports.razor"), "_Imports.razor", FileKinds.Component);
SomeProjectNestedComponentFile3 = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "File3.razor"), "Nested\\File3.razor", FileKinds.Component);
SomeProjectNestedComponentFile4 = new HostDocument(Path.Combine(SomeProjectPath, "Nested", "File4.razor"), "Nested\\File4.razor", FileKinds.Component);
SomeProjectCshtmlComponentFile5 = new HostDocument(Path.Combine(SomeProjectPath, "File5.cshtml"), "File5.cshtml", FileKinds.Component);

var anotherProjectPath = Path.Combine(baseDirectory, "AnotherProject");
var anotherProjectObjPath = Path.Combine(anotherProjectPath, "obj");
Expand All @@ -53,6 +53,7 @@ static TestProjectData()
}

public static readonly HostProject SomeProject;
public static readonly string SomeProjectPath;
public static readonly HostDocument SomeProjectFile1;
public static readonly HostDocument SomeProjectFile2;
public static readonly HostDocument SomeProjectImportFile;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.AspNetCore.Razor.Test.Common;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor.Remote;
using Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.CodeAnalysis.Remote.Razor;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Composition;
Expand All @@ -24,8 +25,11 @@ public abstract class CohostEndpointTestBase(ITestOutputHelper testOutputHelper)
private ExportProvider? _exportProvider;
private TestRemoteServiceInvoker? _remoteServiceInvoker;
private RemoteClientInitializationOptions _clientInitializationOptions;
private IFilePathService? _filePathService;

private protected TestRemoteServiceInvoker RemoteServiceInvoker => _remoteServiceInvoker.AssumeNotNull();
private protected IFilePathService FilePathService => _filePathService.AssumeNotNull();
private protected RemoteLanguageServerFeatureOptions FeatureOptions => OOPExportProvider.GetExportedValue<RemoteLanguageServerFeatureOptions>();

/// <summary>
/// The export provider for Razor OOP services (not Roslyn)
Expand Down Expand Up @@ -53,16 +57,17 @@ protected override async Task InitializeAsync()
UseRazorCohostServer = true
};
UpdateClientInitializationOptions(c => c);

_filePathService = new RemoteFilePathService(FeatureOptions);
}

private protected void UpdateClientInitializationOptions(Func<RemoteClientInitializationOptions, RemoteClientInitializationOptions> mutation)
{
_clientInitializationOptions = mutation(_clientInitializationOptions);
var featureOptions = OOPExportProvider.GetExportedValue<RemoteLanguageServerFeatureOptions>();
featureOptions.SetOptions(_clientInitializationOptions);
FeatureOptions.SetOptions(_clientInitializationOptions);
}

protected TextDocument CreateProjectAndRazorDocument(string contents, string? fileKind = null)
protected TextDocument CreateProjectAndRazorDocument(string contents, string? fileKind = null, (string fileName, string contents)[]? additionalFiles = null)
{
// Using IsLegacy means null == component, so easier for test authors
var isComponent = !FileKinds.IsLegacy(fileKind);
Expand All @@ -84,6 +89,7 @@ protected TextDocument CreateProjectAndRazorDocument(string contents, string? fi
assemblyName: projectName,
LanguageNames.CSharp,
documentFilePath)
.WithDefaultNamespace(TestProjectData.SomeProject.RootNamespace)
.WithMetadataReferences(AspNet80.ReferenceInfos.All.Select(r => r.Reference));

// Importantly, we use Roslyn's remote workspace here so that when our OOP services call into Roslyn, their code
Expand Down Expand Up @@ -121,6 +127,16 @@ @using Microsoft.AspNetCore.Components.Web
"""),
filePath: TestProjectData.SomeProjectImportFile.FilePath);

if (additionalFiles is not null)
{
foreach (var file in additionalFiles)
{
solution = Path.GetExtension(file.fileName) == ".cs"
? solution.AddDocument(DocumentId.CreateNewId(projectId), name: file.fileName, text: SourceText.From(file.contents), filePath: file.fileName)
: solution.AddAdditionalDocument(DocumentId.CreateNewId(projectId), name: file.fileName, text: SourceText.From(file.contents), filePath: file.fileName);
}
}

return solution.GetAdditionalDocument(documentId).AssumeNotNull();
}
}
Loading

0 comments on commit a12e07b

Please sign in to comment.