Skip to content

Commit acc84f6

Browse files
authored
Allow LSP and cohosting to provide specialized methods to get a syntax tree (#10765)
This PR is a classic self-nerd-snipe, and resolves this comment: #10750 (comment) Also inadvertently found a slight "bug" with the existing go to def code in cohosting. Bug is in quotes because the actual user behaviour probably was identical in 99% of cases.
2 parents 1d3c82c + a3edec0 commit acc84f6

File tree

21 files changed

+248
-114
lines changed

21 files changed

+248
-114
lines changed

src/Razor/src/Microsoft.AspNetCore.Razor.LanguageServer/Definition/RazorComponentDefinitionService.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

4-
using System;
5-
using System.Collections.Generic;
6-
using System.Linq;
7-
using System.Text;
8-
using System.Threading.Tasks;
94
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
105
using Microsoft.CodeAnalysis.Razor.GoToDefinition;
116
using Microsoft.CodeAnalysis.Razor.Logging;

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/AbstractRazorComponentDefinitionService.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,8 @@ private async Task<LspRange> GetNavigateRangeAsync(IDocumentSnapshot documentSna
6969
{
7070
_logger.LogInformation($"Attempting to get definition from an attribute directly.");
7171

72-
var originCodeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
73-
7472
var range = await RazorComponentDefinitionHelpers
75-
.TryGetPropertyRangeAsync(originCodeDocument, attributeDescriptor.GetPropertyName(), _documentMappingService, _logger, cancellationToken)
73+
.TryGetPropertyRangeAsync(documentSnapshot, attributeDescriptor.GetPropertyName(), _documentMappingService, _logger, cancellationToken)
7674
.ConfigureAwait(false);
7775

7876
if (range is not null)

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/GoToDefinition/RazorComponentDefinitionHelpers.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
using System.Threading.Tasks;
99
using Microsoft.AspNetCore.Razor.Language;
1010
using Microsoft.AspNetCore.Razor.Language.Syntax;
11-
using Microsoft.CodeAnalysis.CSharp;
1211
using Microsoft.CodeAnalysis.CSharp.Syntax;
1312
using Microsoft.CodeAnalysis.Razor.DocumentMapping;
1413
using Microsoft.CodeAnalysis.Razor.Logging;
14+
using Microsoft.CodeAnalysis.Razor.ProjectSystem;
1515
using Microsoft.CodeAnalysis.Razor.Workspaces;
1616
using Microsoft.VisualStudio.LanguageServer.Protocol;
1717
using LspRange = Microsoft.VisualStudio.LanguageServer.Protocol.Range;
@@ -130,13 +130,13 @@ static bool TryGetTagName(RazorSyntaxNode node, [NotNullWhen(true)] out RazorSyn
130130
}
131131

132132
public static async Task<LspRange?> TryGetPropertyRangeAsync(
133-
RazorCodeDocument codeDocument,
133+
IDocumentSnapshot documentSnapshot,
134134
string propertyName,
135135
IDocumentMappingService documentMappingService,
136136
ILogger logger,
137137
CancellationToken cancellationToken)
138138
{
139-
// Parse the C# file and find the property that matches the name.
139+
// Process the C# tree and find the property that matches the name.
140140
// We don't worry about parameter attributes here for two main reasons:
141141
// 1. We don't have symbolic information, so the best we could do would be checking for any
142142
// attribute named Parameter, regardless of which namespace. It also means we would have
@@ -147,9 +147,10 @@ static bool TryGetTagName(RazorSyntaxNode node, [NotNullWhen(true)] out RazorSyn
147147
// tag helper attribute. If they don't have the [Parameter] attribute then the Razor compiler
148148
// will error, but allowing them to Go To Def on that property regardless, actually helps
149149
// them fix the error.
150-
var csharpText = codeDocument.GetCSharpSourceText();
151-
var syntaxTree = CSharpSyntaxTree.ParseText(csharpText, cancellationToken: cancellationToken);
152-
var root = await syntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
150+
151+
var csharpSyntaxTree = await documentSnapshot.GetCSharpSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
152+
var root = await csharpSyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
153+
var codeDocument = await documentSnapshot.GetGeneratedOutputAsync().ConfigureAwait(false);
153154

154155
// Since we know how the compiler generates the C# source we can be a little specific here, and avoid
155156
// long tree walks. If the compiler ever changes how they generate their code, the tests for this will break
@@ -169,6 +170,7 @@ static bool TryGetTagName(RazorSyntaxNode node, [NotNullWhen(true)] out RazorSyn
169170
return null;
170171
}
171172

173+
var csharpText = codeDocument.GetCSharpSourceText();
172174
var range = csharpText.GetRange(property.Identifier.Span);
173175
if (documentMappingService.TryMapToHostDocumentRange(codeDocument.GetCSharpDocument(), range, out var originalRange))
174176
{

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/DocumentSnapshot.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
using System;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.AspNetCore.Razor.Language;
9+
using Microsoft.CodeAnalysis.CSharp;
810
using Microsoft.CodeAnalysis.Text;
911

1012
namespace Microsoft.CodeAnalysis.Razor.ProjectSystem;
@@ -62,4 +64,11 @@ public IDocumentSnapshot WithText(SourceText text)
6264
{
6365
return new DocumentSnapshot(ProjectInternal, State.WithText(text, VersionStamp.Create()));
6466
}
67+
68+
public async Task<SyntaxTree> GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken)
69+
{
70+
var codeDocument = await GetGeneratedOutputAsync().ConfigureAwait(false);
71+
var csharpText = codeDocument.GetCSharpSourceText();
72+
return CSharpSyntaxTree.ParseText(csharpText, cancellationToken: cancellationToken);
73+
}
6574
}

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/IDocumentSnapshot.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license. See License.txt in the project root for license information.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using System.Threading;
56
using System.Threading.Tasks;
67
using Microsoft.AspNetCore.Razor.Language;
78
using Microsoft.CodeAnalysis.Text;
@@ -20,6 +21,12 @@ internal interface IDocumentSnapshot
2021
Task<VersionStamp> GetTextVersionAsync();
2122
Task<RazorCodeDocument> GetGeneratedOutputAsync();
2223

24+
/// <summary>
25+
/// Gets the Roslyn syntax tree for the generated C# for this Razor document
26+
/// </summary>
27+
/// <remarks>Using this from the LSP server side of things is not ideal. Use sparingly :)</remarks>
28+
Task<SyntaxTree> GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken);
29+
2330
bool TryGetText([NotNullWhen(true)] out SourceText? result);
2431
bool TryGetTextVersion(out VersionStamp result);
2532
bool TryGetGeneratedOutput([NotNullWhen(true)] out RazorCodeDocument? result);

src/Razor/src/Microsoft.CodeAnalysis.Razor.Workspaces/ProjectSystem/ImportDocumentSnapshot.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Diagnostics.CodeAnalysis;
66
using System.IO;
7+
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.AspNetCore.Razor.Language;
910
using Microsoft.CodeAnalysis.Text;
@@ -75,4 +76,7 @@ public bool TryGetGeneratedOutput([NotNullWhen(true)] out RazorCodeDocument? res
7576

7677
public IDocumentSnapshot WithText(SourceText text)
7778
=> throw new NotSupportedException();
79+
80+
public Task<SyntaxTree> GetCSharpSyntaxTreeAsync(CancellationToken cancellationToken)
81+
=> throw new NotSupportedException();
7882
}

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/DocumentHighlight/RemoteDocumentHighlightService.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
using Microsoft.CodeAnalysis.Razor.Protocol;
1212
using Microsoft.CodeAnalysis.Razor.Protocol.DocumentHighlight;
1313
using Microsoft.CodeAnalysis.Razor.Remote;
14-
using Microsoft.CodeAnalysis.Razor.Workspaces;
1514
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
1615
using Microsoft.CodeAnalysis.Text;
1716
using static Microsoft.VisualStudio.LanguageServer.Protocol.VsLspExtensions;
@@ -29,7 +28,6 @@ protected override IRemoteDocumentHighlightService CreateService(in ServiceArgs
2928
}
3029

3130
private readonly IDocumentMappingService _documentMappingService = args.ExportProvider.GetExportedValue<IDocumentMappingService>();
32-
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
3331

3432
public ValueTask<Response> GetHighlightsAsync(
3533
RazorPinnedSolutionInfoWrapper solutionInfo,
@@ -68,7 +66,7 @@ private async ValueTask<Response> GetHighlightsAsync(
6866
var csharpDocument = codeDocument.GetCSharpDocument();
6967
if (_documentMappingService.TryMapToGeneratedDocumentPosition(csharpDocument, index, out var mappedPosition, out _))
7068
{
71-
var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
69+
var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false);
7270

7371
var highlights = await DocumentHighlights.GetHighlightsAsync(generatedDocument, mappedPosition, cancellationToken).ConfigureAwait(false);
7472

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/FoldingRanges/RemoteFoldingRangeService.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Microsoft.CodeAnalysis.Razor.FoldingRanges;
1010
using Microsoft.CodeAnalysis.Razor.Protocol.Folding;
1111
using Microsoft.CodeAnalysis.Razor.Remote;
12-
using Microsoft.CodeAnalysis.Razor.Workspaces;
1312
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
1413
using Microsoft.VisualStudio.LanguageServer.Protocol;
1514
using ExternalHandlers = Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost.Handlers;
@@ -25,7 +24,6 @@ protected override IRemoteFoldingRangeService CreateService(in ServiceArgs args)
2524
}
2625

2726
private readonly IFoldingRangeService _foldingRangeService = args.ExportProvider.GetExportedValue<IFoldingRangeService>();
28-
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
2927

3028
public ValueTask<ImmutableArray<RemoteFoldingRange>> GetFoldingRangesAsync(
3129
RazorPinnedSolutionInfoWrapper solutionInfo,
@@ -43,7 +41,7 @@ private async ValueTask<ImmutableArray<RemoteFoldingRange>> GetFoldingRangesAsyn
4341
ImmutableArray<RemoteFoldingRange> htmlRanges,
4442
CancellationToken cancellationToken)
4543
{
46-
var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
44+
var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false);
4745

4846
var csharpRanges = await ExternalHandlers.FoldingRanges.GetFoldingRangesAsync(generatedDocument, cancellationToken).ConfigureAwait(false);
4947

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/GoToDefinition/RemoteGoToDefinitionService.cs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using Microsoft.CodeAnalysis.Razor.GoToDefinition;
1111
using Microsoft.CodeAnalysis.Razor.Protocol;
1212
using Microsoft.CodeAnalysis.Razor.Remote;
13-
using Microsoft.CodeAnalysis.Razor.Workspaces;
1413
using Microsoft.CodeAnalysis.Remote.Razor.DocumentMapping;
1514
using Microsoft.CodeAnalysis.Remote.Razor.ProjectSystem;
1615
using Microsoft.CodeAnalysis.Text;
@@ -33,7 +32,6 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg
3332
}
3433

3534
private readonly IRazorComponentDefinitionService _componentDefinitionService = args.ExportProvider.GetExportedValue<IRazorComponentDefinitionService>();
36-
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
3735

3836
protected override IDocumentPositionInfoStrategy DocumentPositionInfoStrategy => PreferAttributeNameDocumentPositionInfoStrategy.Instance;
3937

@@ -62,14 +60,6 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg
6260

6361
var positionInfo = GetPositionInfo(codeDocument, hostDocumentIndex);
6462

65-
// First, see if this is a Razor component.
66-
var componentLocation = await _componentDefinitionService.GetDefinitionAsync(context.Snapshot, positionInfo, ignoreAttributes: false, cancellationToken).ConfigureAwait(false);
67-
if (componentLocation is not null)
68-
{
69-
// Convert from VS LSP Location to Roslyn. This can be removed when Razor moves fully onto Roslyn's LSP types.
70-
return Results([RoslynLspFactory.CreateLocation(componentLocation.Uri, componentLocation.Range.ToLinePositionSpan())]);
71-
}
72-
7363
if (positionInfo.LanguageKind == RazorLanguageKind.Html)
7464
{
7565
// Sometimes Html can actually be mapped to C#, like for example component attributes, which map to
@@ -83,9 +73,17 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg
8373
}
8474
}
8575

86-
// If it isn't a Razor component, and it isn't C#, let the server know to delegate to HTML.
87-
if (positionInfo.LanguageKind != RazorLanguageKind.CSharp)
76+
if (positionInfo.LanguageKind is RazorLanguageKind.Html or RazorLanguageKind.Razor)
8877
{
78+
// First, see if this is a Razor component. We ignore attributes here, because they're better served by the C# handler.
79+
var componentLocation = await _componentDefinitionService.GetDefinitionAsync(context.Snapshot, positionInfo, ignoreAttributes: true, cancellationToken).ConfigureAwait(false);
80+
if (componentLocation is not null)
81+
{
82+
// Convert from VS LSP Location to Roslyn. This can be removed when Razor moves fully onto Roslyn's LSP types.
83+
return Results([RoslynLspFactory.CreateLocation(componentLocation.Uri, componentLocation.Range.ToLinePositionSpan())]);
84+
}
85+
86+
// If it isn't a Razor component, and it isn't C#, let the server know to delegate to HTML.
8987
return CallHtml;
9088
}
9189

@@ -96,7 +94,7 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg
9694
}
9795

9896
// Finally, call into C#.
99-
var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
97+
var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false);
10098

10199
var locations = await ExternalHandlers.GoToDefinition
102100
.GetDefinitionsAsync(
@@ -121,7 +119,7 @@ protected override IRemoteGoToDefinitionService CreateService(in ServiceArgs arg
121119
var (uri, range) = location;
122120

123121
var (mappedDocumentUri, mappedRange) = await DocumentMappingService
124-
.MapToHostDocumentUriAndRangeAsync((RemoteDocumentSnapshot)context.Snapshot, uri, range.ToLinePositionSpan(), cancellationToken)
122+
.MapToHostDocumentUriAndRangeAsync(context.Snapshot, uri, range.ToLinePositionSpan(), cancellationToken)
125123
.ConfigureAwait(false);
126124

127125
var mappedLocation = RoslynLspFactory.CreateLocation(mappedDocumentUri, mappedRange);

src/Razor/src/Microsoft.CodeAnalysis.Remote.Razor/InlayHints/RemoteInlayHintService.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@ protected override IRemoteInlayHintService CreateService(in ServiceArgs args)
2626
=> new RemoteInlayHintService(in args);
2727
}
2828

29-
private readonly IFilePathService _filePathService = args.ExportProvider.GetExportedValue<IFilePathService>();
30-
3129
public ValueTask<InlayHint[]?> GetInlayHintsAsync(JsonSerializableRazorPinnedSolutionInfoWrapper solutionInfo, JsonSerializableDocumentId razorDocumentId, InlayHintParams inlayHintParams, bool displayAllOverride, CancellationToken cancellationToken)
3230
=> RunServiceAsync(
3331
solutionInfo,
@@ -52,7 +50,7 @@ protected override IRemoteInlayHintService CreateService(in ServiceArgs args)
5250
return null;
5351
}
5452

55-
var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
53+
var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false);
5654

5755
var textDocument = inlayHintParams.TextDocument.WithUri(generatedDocument.CreateUri());
5856
var range = projectedLinePositionSpan.ToRange();
@@ -106,7 +104,7 @@ public ValueTask<InlayHint> ResolveHintAsync(JsonSerializableRazorPinnedSolution
106104

107105
private async ValueTask<InlayHint> ResolveInlayHintAsync(RemoteDocumentContext context, InlayHint inlayHint, CancellationToken cancellationToken)
108106
{
109-
var generatedDocument = await context.GetGeneratedDocumentAsync(_filePathService, cancellationToken).ConfigureAwait(false);
107+
var generatedDocument = await context.Snapshot.GetGeneratedDocumentAsync().ConfigureAwait(false);
110108

111109
return await InlayHints.ResolveInlayHintAsync(generatedDocument, inlayHint, cancellationToken).ConfigureAwait(false);
112110
}

0 commit comments

Comments
 (0)