Skip to content

Commit d6f3a1b

Browse files
author
N. Taylor Mullen
committed
Enable using directive IntelliSense auto-completion.
- Allow identical `@using` directives in the primary document. This means that if a user types `@using System` twice in the primary document (i.e. `Index.cshtml`) then we'll generate that using statement twice; this enables proper C# IntelliSense to light up on both using statements and allows auto-completion to work because there's no more magical distinction logic for Razor code that is in the current document. - Prior to this change if you had two identical using statements in the same document one would be in no-mans land mapped to nothing (not C#, not HTML) and the other would be mapped to C#; the second a user differentiated the two statements (i.e. adding a `.`) we'd distinctify the using statements resulting in an invalid completion. - This PR has an end-user impact where they will now receive the normal C# warning about having duplicate using. I treated this prior behavior more as a bug because we threw away the first using directive instance which impacted editing and in general was a silly thing to correct for the user. - Added new integration test showing how using directives are not de-duplicated in the primary document. #1255
1 parent af63afd commit d6f3a1b

File tree

9 files changed

+321
-29
lines changed

9 files changed

+321
-29
lines changed

src/Microsoft.AspNetCore.Razor.Language/DefaultRazorIntermediateNodeLoweringPhase.cs

+58-14
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument)
3434

3535
document.Options = codeDocument.GetCodeGenerationOptions() ?? _optionsFeature.GetOptions();
3636

37-
var namespaces = new Dictionary<string, SourceSpan?>(StringComparer.Ordinal);
37+
IReadOnlyList<UsingReference> importedUsings = Array.Empty<UsingReference>();
3838

3939
// The import documents should be inserted logically before the main document.
4040
var imports = codeDocument.GetImportSyntaxTrees();
4141
if (imports != null)
4242
{
43-
var importsVisitor = new ImportsVisitor(document, builder, namespaces, syntaxTree.Options.FeatureFlags);
43+
var importsVisitor = new ImportsVisitor(document, builder, syntaxTree.Options.FeatureFlags);
4444

4545
for (var j = 0; j < imports.Count; j++)
4646
{
@@ -49,26 +49,40 @@ protected override void ExecuteCore(RazorCodeDocument codeDocument)
4949
importsVisitor.FilePath = import.Source.FilePath;
5050
importsVisitor.VisitBlock(import.Root);
5151
}
52+
53+
importedUsings = importsVisitor.Usings;
5254
}
5355

5456
var tagHelperPrefix = tagHelperContext?.Prefix;
55-
var visitor = new MainSourceVisitor(document, builder, namespaces, tagHelperPrefix, syntaxTree.Options.FeatureFlags)
57+
var visitor = new MainSourceVisitor(document, builder, tagHelperPrefix, syntaxTree.Options.FeatureFlags)
5658
{
5759
FilePath = syntaxTree.Source.FilePath,
5860
};
5961

6062
visitor.VisitBlock(syntaxTree.Root);
6163

64+
// 1. Prioritize non-imported usings over imported ones.
65+
// 2. Don't import usings that already exist in primary document.
66+
// 3. Allow duplicate usings in primary document (C# warning).
67+
var usingReferences = new List<UsingReference>(visitor.Usings);
68+
for (var j = importedUsings.Count - 1; j >= 0; j--)
69+
{
70+
if (!usingReferences.Contains(importedUsings[j]))
71+
{
72+
usingReferences.Insert(0, importedUsings[j]);
73+
}
74+
}
75+
6276
// In each lowering piece above, namespaces were tracked. We render them here to ensure every
6377
// lowering action has a chance to add a source location to a namespace. Ultimately, closest wins.
6478

6579
var i = 0;
66-
foreach (var @namespace in namespaces)
80+
foreach (var reference in usingReferences)
6781
{
6882
var @using = new UsingDirectiveIntermediateNode()
6983
{
70-
Content = @namespace.Key,
71-
Source = @namespace.Value,
84+
Content = reference.Namespace,
85+
Source = reference.Source,
7286
};
7387

7488
builder.Insert(i++, @using);
@@ -147,21 +161,51 @@ private void ImportDirectives(DocumentIntermediateNode document)
147161
}
148162
}
149163

164+
private struct UsingReference : IEquatable<UsingReference>
165+
{
166+
public UsingReference(string @namespace, SourceSpan? source)
167+
{
168+
Namespace = @namespace;
169+
Source = source;
170+
}
171+
public string Namespace { get; }
172+
173+
public SourceSpan? Source { get; }
174+
175+
public override bool Equals(object other)
176+
{
177+
if (other is UsingReference reference)
178+
{
179+
return Equals(reference);
180+
}
181+
182+
return false;
183+
}
184+
public bool Equals(UsingReference other)
185+
{
186+
return string.Equals(Namespace, other.Namespace, StringComparison.Ordinal);
187+
}
188+
189+
public override int GetHashCode() => Namespace.GetHashCode();
190+
}
191+
150192
private class LoweringVisitor : ParserVisitor
151193
{
152194
protected readonly IntermediateNodeBuilder _builder;
153195
protected readonly DocumentIntermediateNode _document;
154-
protected readonly Dictionary<string, SourceSpan?> _namespaces;
196+
protected readonly List<UsingReference> _usings;
155197
protected readonly RazorParserFeatureFlags _featureFlags;
156198

157-
public LoweringVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary<string, SourceSpan?> namespaces, RazorParserFeatureFlags featureFlags)
199+
public LoweringVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, RazorParserFeatureFlags featureFlags)
158200
{
159201
_document = document;
160202
_builder = builder;
161-
_namespaces = namespaces;
203+
_usings = new List<UsingReference>();
162204
_featureFlags = featureFlags;
163205
}
164206

207+
public IReadOnlyList<UsingReference> Usings => _usings;
208+
165209
public string FilePath { get; set; }
166210

167211
public override void VisitDirectiveToken(DirectiveTokenChunkGenerator chunkGenerator, Span span)
@@ -212,7 +256,7 @@ public override void VisitImportSpan(AddImportChunkGenerator chunkGenerator, Spa
212256
{
213257
var namespaceImport = chunkGenerator.Namespace.Trim();
214258
var namespaceSpan = BuildSourceSpanFromNode(span);
215-
_namespaces[namespaceImport] = namespaceSpan;
259+
_usings.Add(new UsingReference(namespaceImport, namespaceSpan));
216260
}
217261

218262
public override void VisitAddTagHelperSpan(AddTagHelperChunkGenerator chunkGenerator, Span span)
@@ -353,8 +397,8 @@ private class MainSourceVisitor : LoweringVisitor
353397
{
354398
private readonly string _tagHelperPrefix;
355399

356-
public MainSourceVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary<string, SourceSpan?> namespaces, string tagHelperPrefix, RazorParserFeatureFlags featureFlags)
357-
: base(document, builder, namespaces, featureFlags)
400+
public MainSourceVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, string tagHelperPrefix, RazorParserFeatureFlags featureFlags)
401+
: base(document, builder, featureFlags)
358402
{
359403
_tagHelperPrefix = tagHelperPrefix;
360404
}
@@ -731,8 +775,8 @@ private void AddTagHelperAttributes(IList<TagHelperAttributeNode> attributes, Ta
731775

732776
private class ImportsVisitor : LoweringVisitor
733777
{
734-
public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, Dictionary<string, SourceSpan?> namespaces, RazorParserFeatureFlags featureFlags)
735-
: base(document, new ImportBuilder(builder), namespaces, featureFlags)
778+
public ImportsVisitor(DocumentIntermediateNode document, IntermediateNodeBuilder builder, RazorParserFeatureFlags featureFlags)
779+
: base(document, new ImportBuilder(builder), featureFlags)
736780
{
737781
}
738782

test/Microsoft.AspNetCore.Mvc.Razor.Extensions.Test/IntegrationTests/CodeGenerationIntegrationTest.cs

+27-9
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ public class CodeGenerationIntegrationTest : IntegrationTestBase
2323
private CSharpCompilation BaseCompilation => MvcShim.BaseCompilation.WithAssemblyName("AppCode");
2424

2525
#region Runtime
26+
27+
[Fact]
28+
public void UsingDirectives_Runtime()
29+
{
30+
var compilation = BaseCompilation;
31+
32+
RunRuntimeTest(compilation, new[] { "The using directive for 'System' appeared previously in this namespace" });
33+
}
34+
2635
[Fact]
2736
public void InvalidNamespaceAtEOF_Runtime()
2837
{
@@ -301,6 +310,15 @@ public void RazorPageWithNoLeadingPageDirective_Runtime()
301310
#endregion
302311

303312
#region DesignTime
313+
314+
[Fact]
315+
public void UsingDirectives_DesignTime()
316+
{
317+
var compilation = BaseCompilation;
318+
319+
RunDesignTimeTest(compilation, new[] { "The using directive for 'System' appeared previously in this namespace" });
320+
}
321+
304322
[Fact]
305323
public void InvalidNamespaceAtEOF_DesignTime()
306324
{
@@ -597,7 +615,7 @@ public void RazorPageWithNoLeadingPageDirective_DesignTime()
597615

598616
private void RunRuntimeTest(
599617
CSharpCompilation baseCompilation,
600-
IEnumerable<string> expectedErrors = null)
618+
IEnumerable<string> expectedWarnings = null)
601619
{
602620
Assert.Empty(baseCompilation.GetDiagnostics());
603621

@@ -611,12 +629,12 @@ private void RunRuntimeTest(
611629
// Assert
612630
AssertDocumentNodeMatchesBaseline(document.GetDocumentIntermediateNode());
613631
AssertCSharpDocumentMatchesBaseline(document.GetCSharpDocument());
614-
AssertDocumentCompiles(document, baseCompilation, expectedErrors);
632+
AssertDocumentCompiles(document, baseCompilation, expectedWarnings);
615633
}
616634

617635
private void RunDesignTimeTest(
618636
CSharpCompilation baseCompilation,
619-
IEnumerable<string> expectedErrors = null)
637+
IEnumerable<string> expectedWarnings = null)
620638
{
621639
Assert.Empty(baseCompilation.GetDiagnostics());
622640

@@ -631,13 +649,13 @@ private void RunDesignTimeTest(
631649
AssertDocumentNodeMatchesBaseline(document.GetDocumentIntermediateNode());
632650
AssertCSharpDocumentMatchesBaseline(document.GetCSharpDocument());
633651
AssertSourceMappingsMatchBaseline(document);
634-
AssertDocumentCompiles(document, baseCompilation, expectedErrors);
652+
AssertDocumentCompiles(document, baseCompilation, expectedWarnings);
635653
}
636654

637655
private void AssertDocumentCompiles(
638656
RazorCodeDocument document,
639657
CSharpCompilation baseCompilation,
640-
IEnumerable<string> expectedErrors = null)
658+
IEnumerable<string> expectedWarnings = null)
641659
{
642660
var cSharp = document.GetCSharpDocument().GeneratedCode;
643661

@@ -649,15 +667,15 @@ private void AssertDocumentCompiles(
649667

650668
var diagnostics = compilation.GetDiagnostics();
651669

652-
var errors = diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning);
670+
var warnings = diagnostics.Where(d => d.Severity >= DiagnosticSeverity.Warning);
653671

654-
if (expectedErrors == null)
672+
if (expectedWarnings == null)
655673
{
656-
Assert.Empty(errors.Select(e => e.GetMessage()));
674+
Assert.Empty(warnings);
657675
}
658676
else
659677
{
660-
Assert.Equal(expectedErrors, errors.Select(e => e.GetMessage()));
678+
Assert.Equal(expectedWarnings, warnings.Select(e => e.GetMessage()));
661679
}
662680
}
663681

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
@using System.ComponentModel
2+
@using System.Collections
3+
@using System
4+
@using System
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// <auto-generated/>
2+
#pragma warning disable 1591
3+
namespace AspNetCore
4+
{
5+
#line hidden
6+
using TModel = global::System.Object;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Threading.Tasks;
10+
using Microsoft.AspNetCore.Mvc;
11+
using Microsoft.AspNetCore.Mvc.Rendering;
12+
using Microsoft.AspNetCore.Mvc.ViewFeatures;
13+
#line 1 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/UsingDirectives.cshtml"
14+
using System.ComponentModel;
15+
16+
#line default
17+
#line hidden
18+
#line 2 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/UsingDirectives.cshtml"
19+
using System.Collections;
20+
21+
#line default
22+
#line hidden
23+
#line 3 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/UsingDirectives.cshtml"
24+
using System;
25+
26+
#line default
27+
#line hidden
28+
#line 4 "TestFiles/IntegrationTests/CodeGenerationIntegrationTest/UsingDirectives.cshtml"
29+
using System;
30+
31+
#line default
32+
#line hidden
33+
public class TestFiles_IntegrationTests_CodeGenerationIntegrationTest_UsingDirectives : global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<dynamic>
34+
{
35+
#pragma warning disable 219
36+
private void __RazorDirectiveTokenHelpers__() {
37+
}
38+
#pragma warning restore 219
39+
#pragma warning disable 0414
40+
private static System.Object __o = null;
41+
#pragma warning restore 0414
42+
#pragma warning disable 1998
43+
public async override global::System.Threading.Tasks.Task ExecuteAsync()
44+
{
45+
}
46+
#pragma warning restore 1998
47+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
48+
public global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider ModelExpressionProvider { get; private set; }
49+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
50+
public global::Microsoft.AspNetCore.Mvc.IUrlHelper Url { get; private set; }
51+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
52+
public global::Microsoft.AspNetCore.Mvc.IViewComponentHelper Component { get; private set; }
53+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
54+
public global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper Json { get; private set; }
55+
[global::Microsoft.AspNetCore.Mvc.Razor.Internal.RazorInjectAttribute]
56+
public global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<dynamic> Html { get; private set; }
57+
}
58+
}
59+
#pragma warning restore 1591
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
Document -
2+
NamespaceDeclaration - - AspNetCore
3+
UsingDirective - - TModel = global::System.Object
4+
UsingDirective - (16:1,1 [32] ) - System.Collections.Generic
5+
UsingDirective - (51:2,1 [17] ) - System.Linq
6+
UsingDirective - (71:3,1 [28] ) - System.Threading.Tasks
7+
UsingDirective - (102:4,1 [30] ) - Microsoft.AspNetCore.Mvc
8+
UsingDirective - (135:5,1 [40] ) - Microsoft.AspNetCore.Mvc.Rendering
9+
UsingDirective - (178:6,1 [43] ) - Microsoft.AspNetCore.Mvc.ViewFeatures
10+
UsingDirective - (1:0,1 [27] UsingDirectives.cshtml) - System.ComponentModel
11+
UsingDirective - (31:1,1 [24] UsingDirectives.cshtml) - System.Collections
12+
UsingDirective - (58:2,1 [12] UsingDirectives.cshtml) - System
13+
UsingDirective - (73:3,1 [12] UsingDirectives.cshtml) - System
14+
ClassDeclaration - - public - TestFiles_IntegrationTests_CodeGenerationIntegrationTest_UsingDirectives - global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<dynamic> -
15+
DesignTimeDirective -
16+
DirectiveToken - (231:7,8 [62] ) - global::Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper<TModel>
17+
DirectiveToken - (294:7,71 [4] ) - Html
18+
DirectiveToken - (308:8,8 [54] ) - global::Microsoft.AspNetCore.Mvc.Rendering.IJsonHelper
19+
DirectiveToken - (363:8,63 [4] ) - Json
20+
DirectiveToken - (377:9,8 [53] ) - global::Microsoft.AspNetCore.Mvc.IViewComponentHelper
21+
DirectiveToken - (431:9,62 [9] ) - Component
22+
DirectiveToken - (450:10,8 [43] ) - global::Microsoft.AspNetCore.Mvc.IUrlHelper
23+
DirectiveToken - (494:10,52 [3] ) - Url
24+
DirectiveToken - (507:11,8 [70] ) - global::Microsoft.AspNetCore.Mvc.ViewFeatures.IModelExpressionProvider
25+
DirectiveToken - (578:11,79 [23] ) - ModelExpressionProvider
26+
DirectiveToken - (617:12,14 [96] ) - Microsoft.AspNetCore.Mvc.Razor.TagHelpers.UrlResolutionTagHelper, Microsoft.AspNetCore.Mvc.Razor
27+
DirectiveToken - (729:13,14 [87] ) - Microsoft.AspNetCore.Mvc.Razor.TagHelpers.HeadTagHelper, Microsoft.AspNetCore.Mvc.Razor
28+
DirectiveToken - (832:14,14 [87] ) - Microsoft.AspNetCore.Mvc.Razor.TagHelpers.BodyTagHelper, Microsoft.AspNetCore.Mvc.Razor
29+
CSharpCode -
30+
IntermediateToken - - CSharp - #pragma warning disable 0414
31+
CSharpCode -
32+
IntermediateToken - - CSharp - private static System.Object __o = null;
33+
CSharpCode -
34+
IntermediateToken - - CSharp - #pragma warning restore 0414
35+
MethodDeclaration - - public async override - global::System.Threading.Tasks.Task - ExecuteAsync
36+
HtmlContent - (28:0,28 [2] UsingDirectives.cshtml)
37+
IntermediateToken - (28:0,28 [2] UsingDirectives.cshtml) - Html - \n
38+
HtmlContent - (55:1,25 [2] UsingDirectives.cshtml)
39+
IntermediateToken - (55:1,25 [2] UsingDirectives.cshtml) - Html - \n
40+
HtmlContent - (70:2,13 [2] UsingDirectives.cshtml)
41+
IntermediateToken - (70:2,13 [2] UsingDirectives.cshtml) - Html - \n
42+
Inject -
43+
Inject -
44+
Inject -
45+
Inject -
46+
Inject -
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Source Location: (1:0,1 [27] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/UsingDirectives.cshtml)
2+
|using System.ComponentModel|
3+
Generated Location: (461:13,0 [27] )
4+
|using System.ComponentModel|
5+
6+
Source Location: (31:1,1 [24] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/UsingDirectives.cshtml)
7+
|using System.Collections|
8+
Generated Location: (613:18,0 [24] )
9+
|using System.Collections|
10+
11+
Source Location: (58:2,1 [12] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/UsingDirectives.cshtml)
12+
|using System|
13+
Generated Location: (762:23,0 [12] )
14+
|using System|
15+
16+
Source Location: (73:3,1 [12] TestFiles/IntegrationTests/CodeGenerationIntegrationTest/UsingDirectives.cshtml)
17+
|using System|
18+
Generated Location: (899:28,0 [12] )
19+
|using System|
20+

0 commit comments

Comments
 (0)