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

Cohosting tests for Uri presentation #10642

Merged
merged 6 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -12,6 +12,7 @@
using Microsoft.AspNetCore.Razor.Test.Common.Workspaces;
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 @@ -25,8 +26,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 @@ -54,16 +58,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 @@ -85,6 +90,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));

var solution = Workspace.CurrentSolution.AddProject(projectInfo);
Expand Down Expand Up @@ -119,6 +125,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