1
+ // Licensed to the .NET Foundation under one or more agreements.
2
+ // The .NET Foundation licenses this file to you under the MIT license.
3
+
4
+ using System . Threading ;
5
+ using System . Threading . Tasks ;
6
+ using Microsoft . AspNetCore . Razor . Language ;
7
+ using Microsoft . AspNetCore . Razor . Language . Syntax ;
8
+ using Microsoft . CodeAnalysis . ExternalAccess . Razor ;
9
+ using Microsoft . CodeAnalysis . Razor . Protocol ;
10
+ using Microsoft . CodeAnalysis . Razor . Remote ;
11
+ using Microsoft . CodeAnalysis . Razor . Workspaces ;
12
+ using Microsoft . CodeAnalysis . Remote . Razor . ProjectSystem ;
13
+ using Microsoft . CodeAnalysis . Text ;
14
+ using Response = Microsoft . CodeAnalysis . Razor . Remote . RemoteResponse < bool > ;
15
+
16
+ namespace Microsoft . CodeAnalysis . Remote . Razor ;
17
+
18
+ internal sealed partial class RemoteWrapWithTagService ( in ServiceArgs args ) : RazorDocumentServiceBase ( in args ) , IRemoteWrapWithTagService
19
+ {
20
+ internal sealed class Factory : FactoryBase < IRemoteWrapWithTagService >
21
+ {
22
+ protected override IRemoteWrapWithTagService CreateService ( in ServiceArgs args )
23
+ => new RemoteWrapWithTagService ( in args ) ;
24
+ }
25
+
26
+ public ValueTask < Response > IsValidWrapWithTagLocationAsync (
27
+ RazorPinnedSolutionInfoWrapper solutionInfo ,
28
+ DocumentId razorDocumentId ,
29
+ LspRange range ,
30
+ CancellationToken cancellationToken )
31
+ => RunServiceAsync (
32
+ solutionInfo ,
33
+ razorDocumentId ,
34
+ context => IsValidWrapWithTagLocationAsync ( context , range , cancellationToken ) ,
35
+ cancellationToken ) ;
36
+
37
+ private async ValueTask < Response > IsValidWrapWithTagLocationAsync (
38
+ RemoteDocumentContext context ,
39
+ LspRange range ,
40
+ CancellationToken cancellationToken )
41
+ {
42
+ var codeDocument = await context . GetCodeDocumentAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
43
+ var sourceText = codeDocument . Source . Text ;
44
+
45
+ if ( range ? . Start is not { } start ||
46
+ ! sourceText . TryGetAbsoluteIndex ( start , out var hostDocumentIndex ) )
47
+ {
48
+ return Response . NoFurtherHandling ;
49
+ }
50
+
51
+ // First thing we do is make sure we start at a non-whitespace character. This is important because in some
52
+ // situations the whitespace can be technically C#, but move one character to the right and it's HTML. eg
53
+ //
54
+ // @if (true) {
55
+ // | <p></p>
56
+ // }
57
+ //
58
+ // Limiting this to only whitespace on the same line, as it's not clear what user expectation would be otherwise.
59
+ var requestSpan = sourceText . GetTextSpan ( range ) ;
60
+ if ( sourceText . TryGetFirstNonWhitespaceOffset ( requestSpan , out var offset , out var newLineCount ) &&
61
+ newLineCount == 0 )
62
+ {
63
+ hostDocumentIndex += offset ;
64
+ }
65
+
66
+ // Since we're at the start of the selection, lets prefer the language to the right of the cursor if possible.
67
+ // That way with the following situation:
68
+ //
69
+ // @if (true) {
70
+ // |<p></p>
71
+ // }
72
+ //
73
+ // Instead of C#, which certainly would be expected to go in an if statement, we'll see HTML, which obviously
74
+ // is the better choice for this operation.
75
+ var languageKind = codeDocument . GetLanguageKind ( hostDocumentIndex , rightAssociative : true ) ;
76
+
77
+ // However, reverse scenario is possible as well, when we have
78
+ // <div>
79
+ // |@if (true) {}
80
+ // <p></p>
81
+ // </div>
82
+ // in which case right-associative GetLanguageKind will return Razor and left-associative will return HTML
83
+ // We should hand that case as well, see https://github.com/dotnet/razor/issues/10819
84
+ if ( languageKind is RazorLanguageKind . Razor )
85
+ {
86
+ languageKind = codeDocument . GetLanguageKind ( hostDocumentIndex , rightAssociative : false ) ;
87
+ }
88
+
89
+ if ( languageKind is not RazorLanguageKind . Html )
90
+ {
91
+ // In general, we don't support C# for obvious reasons, but we can support implicit expressions. ie
92
+ //
93
+ // <p>@curr$$entCount</p>
94
+ //
95
+ // We can expand the range to encompass the whole implicit expression, and then it will wrap as expected.
96
+ // Similarly if they have selected the implicit expression, then we can continue. ie
97
+ //
98
+ // <p>[|@currentCount|]</p>
99
+
100
+ var tree = await context . GetSyntaxTreeAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
101
+ var node = tree . Root . FindNode ( requestSpan , includeWhitespace : false , getInnermostNodeForTie : true ) ;
102
+ if ( node ? . FirstAncestorOrSelf < CSharpImplicitExpressionSyntax > ( ) is { Parent : CSharpCodeBlockSyntax codeBlock } &&
103
+ ( requestSpan == codeBlock . Span || requestSpan . Length == 0 ) )
104
+ {
105
+ // Pretend we're in Html so the rest of the logic can continue
106
+ languageKind = RazorLanguageKind . Html ;
107
+ }
108
+ }
109
+
110
+ if ( languageKind is not RazorLanguageKind . Html )
111
+ {
112
+ return Response . NoFurtherHandling ;
113
+ }
114
+
115
+ return Response . Results ( true ) ;
116
+ }
117
+ }
0 commit comments