Skip to content

Commit 17ab875

Browse files
authored
UriPresentation to Cohost/OOP, Part 1 (#10336)
Part of #9519 This enables the Uri Presentation endpoint in cohosting, which is used when draging and dropping a file only a Razor document, and handles placing a Razor component tag in the document. It's part 1 because part 2 involves calling out to Web Tools to allow the Html language server to do the same, but I haven't done that bit yet. No point reviewing it all in one huge lump though. This is also a reasonable "bare minimum" changes necessary to create a new cohost endpoint and service.
2 parents cfc8416 + df162fb commit 17ab875

File tree

11 files changed

+301
-32
lines changed

11 files changed

+301
-32
lines changed

eng/targets/Services.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@
1919
<ServiceHubService Include="Microsoft.VisualStudio.Razor.HtmlDocument" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteHtmlDocumentServiceFactory" />
2020
<ServiceHubService Include="Microsoft.VisualStudio.Razor.TagHelperProvider" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteTagHelperProviderServiceFactory"/>
2121
<ServiceHubService Include="Microsoft.VisualStudio.Razor.ClientInitialization" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteClientInitializationServiceFactory" />
22+
<ServiceHubService Include="Microsoft.VisualStudio.Razor.UriPresentation" ClassName="Microsoft.CodeAnalysis.Remote.Razor.RemoteUriPresentationServiceFactory" />
2223
</ItemGroup>
2324
</Project>

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/DocumentPresentation/TextDocumentUriPresentationEndpoint.cs

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66
using System.Linq;
77
using System.Threading;
88
using System.Threading.Tasks;
9-
using Microsoft.AspNetCore.Razor.Language;
109
using Microsoft.AspNetCore.Razor.LanguageServer.Hosting;
11-
using Microsoft.AspNetCore.Razor.PooledObjects;
1210
using Microsoft.CodeAnalysis.Razor;
1311
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
12+
using Microsoft.CodeAnalysis.Razor.DocumentPresentation;
1413
using Microsoft.CodeAnalysis.Razor.Logging;
1514
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
1615
using Microsoft.CodeAnalysis.Razor.Protocol;
@@ -52,34 +51,9 @@ protected override IRazorPresentationParams CreateRazorRequestParameters(UriPres
5251

5352
protected override async Task<WorkspaceEdit?> TryGetRazorWorkspaceEditAsync(RazorLanguageKind languageKind, UriPresentationParams request, CancellationToken cancellationToken)
5453
{
55-
if (languageKind is not RazorLanguageKind.Html)
56-
{
57-
// We don't do anything for HTML
58-
return null;
59-
}
60-
61-
if (request.Uris is null || request.Uris.Length == 0)
62-
{
63-
Logger.LogInformation($"No URIs were included in the request?");
64-
return null;
65-
}
66-
67-
var razorFileUri = request.Uris.Where(
68-
x => Path.GetFileName(x.GetAbsoluteOrUNCPath()).EndsWith(".razor", FilePathComparison.Instance)).FirstOrDefault();
69-
70-
// We only want to handle requests for a single .razor file, but when there are files nested under a .razor
71-
// file (for example, Goo.razor.css, Goo.razor.cs etc.) then we'll get all of those files as well, when the user
72-
// thinks they're just dragging the parent one, so we have to be a little bit clever with the filter here
54+
var razorFileUri = UriPresentationHelper.GetComponentFileNameFromUriPresentationRequest(languageKind, request.Uris, Logger);
7355
if (razorFileUri == null)
7456
{
75-
Logger.LogInformation($"No file in the drop was a razor file URI.");
76-
return null;
77-
}
78-
79-
var fileName = Path.GetFileName(razorFileUri.GetAbsoluteOrUNCPath());
80-
if (request.Uris.Any(uri => !Path.GetFileName(uri.GetAbsoluteOrUNCPath()).StartsWith(fileName, FilePathComparison.Instance)))
81-
{
82-
Logger.LogInformation($"One or more URIs were not a child file of the main .razor file.");
8357
return null;
8458
}
8559

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Extensions/IServiceCollectionExtensions.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,13 @@ public static void AddCodeActionsServices(this IServiceCollection services)
167167
services.AddSingleton<HtmlCodeActionResolver, DefaultHtmlCodeActionResolver>();
168168
}
169169

170-
public static void AddTextDocumentServices(this IServiceCollection services)
170+
public static void AddTextDocumentServices(this IServiceCollection services, LanguageServerFeatureOptions featureOptions)
171171
{
172172
services.AddHandlerWithCapabilities<TextDocumentTextPresentationEndpoint>();
173-
services.AddHandlerWithCapabilities<TextDocumentUriPresentationEndpoint>();
173+
if (!featureOptions.UseRazorCohostServer)
174+
{
175+
services.AddHandlerWithCapabilities<TextDocumentUriPresentationEndpoint>();
176+
}
174177

175178
services.AddHandlerWithCapabilities<DocumentSpellCheckEndpoint>();
176179
services.AddHandler<WorkspaceSpellCheckEndpoint>();

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/RazorLanguageServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ protected override ILspServices ConstructLspServices()
123123
services.AddCodeActionsServices();
124124
services.AddOptionsServices(_lspOptions);
125125
services.AddHoverServices();
126-
services.AddTextDocumentServices();
126+
services.AddTextDocumentServices(featureOptions);
127127

128128
// Auto insert
129129
services.AddSingleton<IOnAutoInsertProvider, CloseTextTagOnAutoInsertProvider>();
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.IO;
6+
using System.Linq;
7+
using Microsoft.CodeAnalysis.Razor.Logging;
8+
using Microsoft.CodeAnalysis.Razor.Protocol;
9+
10+
namespace Microsoft.CodeAnalysis.Razor.DocumentPresentation;
11+
12+
internal static class UriPresentationHelper
13+
{
14+
public static Uri? GetComponentFileNameFromUriPresentationRequest(RazorLanguageKind languageKind, Uri[]? uris, ILogger logger)
15+
{
16+
if (languageKind is not RazorLanguageKind.Html)
17+
{
18+
// Component tags can only be inserted into Html contexts, so if this isn't Html there is nothing we can do.
19+
return null;
20+
}
21+
22+
if (uris is null || uris.Length == 0)
23+
{
24+
logger.LogDebug($"No URIs were included in the request?");
25+
return null;
26+
}
27+
28+
var razorFileUri = uris.Where(
29+
x => Path.GetFileName(x.GetAbsoluteOrUNCPath()).EndsWith(".razor", FilePathComparison.Instance)).FirstOrDefault();
30+
31+
// We only want to handle requests for a single .razor file, but when there are files nested under a .razor
32+
// file (for example, Goo.razor.css, Goo.razor.cs etc.) then we'll get all of those files as well, when the user
33+
// thinks they're just dragging the parent one, so we have to be a little bit clever with the filter here
34+
if (razorFileUri == null)
35+
{
36+
logger.LogDebug($"No file in the drop was a razor file URI.");
37+
return null;
38+
}
39+
40+
var fileName = Path.GetFileName(razorFileUri.GetAbsoluteOrUNCPath());
41+
if (uris.Any(uri => !Path.GetFileName(uri.GetAbsoluteOrUNCPath()).StartsWith(fileName, FilePathComparison.Instance)))
42+
{
43+
logger.LogDebug($"One or more URIs were not a child file of the main .razor file.");
44+
return null;
45+
}
46+
47+
return razorFileUri;
48+
}
49+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
namespace Microsoft.CodeAnalysis.Razor.Remote;
11+
12+
internal interface IRemoteUriPresentationService
13+
{
14+
ValueTask<TextChange?> GetPresentationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken);
15+
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/Remote/RazorServices.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,6 @@ internal static class RazorServices
2323
(typeof(IRemoteClientInitializationService), null),
2424
(typeof(IRemoteSemanticTokensService), null),
2525
(typeof(IRemoteHtmlDocumentService), null),
26+
(typeof(IRemoteUriPresentationService), null),
2627
]);
2728
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/SemanticTokens/RemoteSemanticTokensService.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ internal sealed class RemoteSemanticTokensService(
4141

4242
var documentContext = Create(razorDocument);
4343

44-
// TODO: Telemetry?
4544
return await _razorSemanticTokensInfoService.GetSemanticTokensAsync(documentContext, span, colorBackground, correlationId, cancellationToken).ConfigureAwait(false);
4645
}
4746

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.CodeAnalysis.ExternalAccess.Razor;
8+
using Microsoft.CodeAnalysis.ExternalAccess.Razor.Api;
9+
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
10+
using Microsoft.CodeAnalysis.Razor.DocumentPresentation;
11+
using Microsoft.CodeAnalysis.Razor.Logging;
12+
using Microsoft.CodeAnalysis.Razor.Remote;
13+
using Microsoft.CodeAnalysis.Razor.Workspaces;
14+
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
15+
using Microsoft.CodeAnalysis.Text;
16+
using Microsoft.ServiceHub.Framework;
17+
18+
namespace Microsoft.CodeAnalysis.Remote.Razor;
19+
20+
internal sealed class RemoteUriPresentationService(
21+
IServiceBroker serviceBroker,
22+
IRazorDocumentMappingService documentMappingService,
23+
DocumentSnapshotFactory documentSnapshotFactory,
24+
ILoggerFactory loggerFactory)
25+
: RazorServiceBase(serviceBroker), IRemoteUriPresentationService
26+
{
27+
private readonly IRazorDocumentMappingService _documentMappingService = documentMappingService;
28+
private readonly DocumentSnapshotFactory _documentSnapshotFactory = documentSnapshotFactory;
29+
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<RemoteUriPresentationService>();
30+
31+
public ValueTask<TextChange?> GetPresentationAsync(RazorPinnedSolutionInfoWrapper solutionInfo, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken)
32+
=> RazorBrokeredServiceImplementation.RunServiceAsync(
33+
solutionInfo,
34+
ServiceBrokerClient,
35+
solution => GetPresentationAsync(solution, razorDocumentId, span, uris, cancellationToken),
36+
cancellationToken);
37+
38+
private async ValueTask<TextChange?> GetPresentationAsync(Solution solution, DocumentId razorDocumentId, LinePositionSpan span, Uri[]? uris, CancellationToken cancellationToken)
39+
{
40+
var razorDocument = solution.GetAdditionalDocument(razorDocumentId);
41+
if (razorDocument is null)
42+
{
43+
return null;
44+
}
45+
46+
var snapshot = _documentSnapshotFactory.GetOrCreate(razorDocument);
47+
var codeDocument = await snapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
48+
var sourceText = await razorDocument.GetTextAsync(cancellationToken).ConfigureAwait(false);
49+
50+
if (!sourceText.TryGetAbsoluteIndex(span.Start.Line, span.Start.Character, out var index))
51+
{
52+
return null;
53+
}
54+
55+
var languageKind = _documentMappingService.GetLanguageKind(codeDocument, index, rightAssociative: true);
56+
57+
var razorFileUri = UriPresentationHelper.GetComponentFileNameFromUriPresentationRequest(languageKind, uris, _logger);
58+
if (razorFileUri is null)
59+
{
60+
return null;
61+
}
62+
63+
var ids = razorDocument.Project.Solution.GetDocumentIdsWithFilePath(GetDocumentFilePathFromUri(razorFileUri));
64+
if (ids.Length == 0)
65+
{
66+
return null;
67+
}
68+
69+
// We assume linked documents would produce the same component tag so just take the first
70+
var otherDocument = razorDocument.Project.Solution.GetAdditionalDocument(ids[0]);
71+
if (otherDocument is null)
72+
{
73+
return null;
74+
}
75+
76+
var otherSnapshot = _documentSnapshotFactory.GetOrCreate(otherDocument);
77+
var descriptor = await otherSnapshot.TryGetTagHelperDescriptorAsync(cancellationToken).ConfigureAwait(false);
78+
79+
if (descriptor is null)
80+
{
81+
return null;
82+
}
83+
84+
var tag = descriptor.TryGetComponentTag();
85+
if (tag is null)
86+
{
87+
return null;
88+
}
89+
90+
return new TextChange(span.ToTextSpan(sourceText), tag);
91+
}
92+
93+
// TODO: Call the real Roslyn API once https://github.com/dotnet/roslyn/pull/73289 is merged
94+
public static string GetDocumentFilePathFromUri(Uri uri)
95+
=> uri.IsFile ? uri.LocalPath : uri.AbsoluteUri;
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT license. See License.txt in the project root for license information.
3+
4+
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
5+
using Microsoft.CodeAnalysis.Razor.Logging;
6+
using Microsoft.CodeAnalysis.Razor.Remote;
7+
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
8+
using Microsoft.ServiceHub.Framework;
9+
using Microsoft.VisualStudio.Composition;
10+
11+
namespace Microsoft.CodeAnalysis.Remote.Razor;
12+
13+
internal sealed class RemoteUriPresentationServiceFactory : RazorServiceFactoryBase<IRemoteUriPresentationService>
14+
{
15+
// WARNING: We must always have a parameterless constructor in order to be properly handled by ServiceHub.
16+
public RemoteUriPresentationServiceFactory()
17+
: base(RazorServices.Descriptors)
18+
{
19+
}
20+
21+
protected override IRemoteUriPresentationService CreateService(IServiceBroker serviceBroker, ExportProvider exportProvider)
22+
{
23+
var documentMappingService = exportProvider.GetExportedValue<IRazorDocumentMappingService>();
24+
var documentSnapshotFactory = exportProvider.GetExportedValue<DocumentSnapshotFactory>();
25+
var loggerFactory = exportProvider.GetExportedValue<ILoggerFactory>();
26+
return new RemoteUriPresentationService(serviceBroker, documentMappingService, documentSnapshotFactory, loggerFactory);
27+
}
28+
}

0 commit comments

Comments
 (0)