Skip to content

Commit b422f78

Browse files
Delay loading of CodeRefactoringProvider's till absolutely needed (#78495)
Delay loading of CodeRefactoringProvider's till absolutely needed. For example, we do not want to load refactoring provider for AdditonalDocument XAML files unless user is editing such files.
2 parents 1b379e0 + 92a7523 commit b422f78

File tree

5 files changed

+154
-17
lines changed

5 files changed

+154
-17
lines changed

src/EditorFeatures/Test/CodeRefactorings/CodeRefactoringServiceTest.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Collections.Immutable;
99
using System.Composition;
1010
using System.Linq;
11+
using System.Reflection;
1112
using System.Threading;
1213
using System.Threading.Tasks;
1314
using Microsoft.CodeAnalysis.CodeActions;
@@ -229,6 +230,63 @@ public async Task TestAnalyzerConfigDocumentRefactoringAsync()
229230
Assert.Equal(refactoring2.Title, globalConfigRefactoringTitle);
230231
}
231232

233+
[Fact]
234+
public void TestDelayedLoading()
235+
{
236+
var composition = FeaturesTestCompositions.Features.AddParts(
237+
typeof(NonSourceFileRefactoringWithDocumentKindsAndExtensions),
238+
typeof(NonSourceFileRefactoringWithDocumentKinds),
239+
typeof(NonSourceFileRefactoringWithDocumentExtensions),
240+
typeof(NonSourceFileRefactoringWithoutDocumentKindsAndExtensions));
241+
242+
using var workspace = TestWorkspace.CreateCSharp("", composition: composition);
243+
var refactoringService = (CodeRefactorings.CodeRefactoringService)workspace.GetService<ICodeRefactoringService>();
244+
245+
var project = workspace.CurrentSolution.Projects.Single()
246+
.AddAdditionalDocument("test.TXT", "", filePath: "test.TXT").Project
247+
.AddAdditionalDocument("test", "", filePath: "test").Project
248+
.AddAdditionalDocument("test.log", "", filePath: "test.log").Project
249+
.AddDocument("test.cs", "", filePath: "test.cs").Project
250+
.AddDocument("test.editorconfig", "", filePath: "test.editorconfig").Project;
251+
252+
VerifyProviders(refactoringService,
253+
project.AdditionalDocuments.Single(t => t.Name == "test.TXT"),
254+
typeof(NonSourceFileRefactoringWithDocumentKindsAndExtensions),
255+
typeof(NonSourceFileRefactoringWithDocumentKinds));
256+
257+
VerifyProviders(refactoringService,
258+
project.AdditionalDocuments.Single(t => t.Name == "test"),
259+
typeof(NonSourceFileRefactoringWithDocumentKinds));
260+
261+
VerifyProviders(refactoringService,
262+
project.AdditionalDocuments.Single(t => t.Name == "test.log"),
263+
typeof(NonSourceFileRefactoringWithDocumentKinds));
264+
265+
VerifyProviders(refactoringService,
266+
project.Documents.Single(t => t.Name == "test.editorconfig"),
267+
typeof(NonSourceFileRefactoringWithDocumentExtensions),
268+
typeof(NonSourceFileRefactoringWithoutDocumentKindsAndExtensions));
269+
270+
VerifyProviders(refactoringService,
271+
project.Documents.Single(t => t.Name == "test.cs"),
272+
typeof(NonSourceFileRefactoringWithoutDocumentKindsAndExtensions));
273+
}
274+
275+
private static void VerifyProviders(CodeRefactorings.CodeRefactoringService service, TextDocument document, params Type[] expectedProviderTypes)
276+
{
277+
// Exclude providers which have not been setup by test
278+
var assembly = Assembly.GetExecutingAssembly();
279+
var actualProviders = service.GetProviders(document)
280+
.Where(p => p.GetType().Assembly == assembly)
281+
.ToArray();
282+
283+
foreach (var type in expectedProviderTypes)
284+
{
285+
Assert.Contains(actualProviders, p => p.GetType() == type);
286+
}
287+
Assert.Equal(expectedProviderTypes.Length, actualProviders.Length);
288+
}
289+
232290
internal abstract class AbstractNonSourceFileRefactoring : CodeRefactoringProvider
233291
{
234292
public string Title { get; }

src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,43 +30,73 @@ namespace Microsoft.CodeAnalysis.CodeRefactorings;
3030
internal sealed class CodeRefactoringService(
3131
[ImportMany] IEnumerable<Lazy<CodeRefactoringProvider, CodeChangeProviderMetadata>> providers) : ICodeRefactoringService
3232
{
33-
private readonly Lazy<ImmutableDictionary<string, Lazy<ImmutableArray<CodeRefactoringProvider>>>> _lazyLanguageToProvidersMap = new Lazy<ImmutableDictionary<string, Lazy<ImmutableArray<CodeRefactoringProvider>>>>(
34-
() =>
35-
ImmutableDictionary.CreateRange(
36-
DistributeLanguages(providers)
37-
.GroupBy(lz => lz.Metadata.Language)
38-
.Select(grp => KeyValuePairUtil.Create(
39-
grp.Key,
40-
new Lazy<ImmutableArray<CodeRefactoringProvider>>(() => [.. ExtensionOrderer.Order(grp).Select(lz => lz.Value)])))));
33+
private readonly Lazy<ImmutableDictionary<ProviderKey, Lazy<ImmutableArray<CodeRefactoringProvider>>>> _lazyLanguageDocumentToProvidersMap =
34+
new Lazy<ImmutableDictionary<ProviderKey, Lazy<ImmutableArray<CodeRefactoringProvider>>>>(() =>
35+
ImmutableDictionary.CreateRange(
36+
DistributeLanguagesAndDocuments(providers)
37+
.GroupBy(lz => new ProviderKey(lz.Metadata.Language, lz.Metadata.DocumentKind, lz.Metadata.DocumentExtension))
38+
.Select(grp => KeyValuePairUtil.Create(grp.Key,
39+
new Lazy<ImmutableArray<CodeRefactoringProvider>>(() => [.. ExtensionOrderer.Order(grp).Select(lz => lz.Value)])))));
40+
4141
private readonly Lazy<ImmutableDictionary<CodeRefactoringProvider, CodeChangeProviderMetadata>> _lazyRefactoringToMetadataMap = new(() => providers.Where(provider => provider.IsValueCreated).ToImmutableDictionary(provider => provider.Value, provider => provider.Metadata));
4242

4343
private ImmutableDictionary<CodeRefactoringProvider, FixAllProviderInfo?> _fixAllProviderMap = ImmutableDictionary<CodeRefactoringProvider, FixAllProviderInfo?>.Empty;
4444

45-
private static IEnumerable<Lazy<CodeRefactoringProvider, OrderableLanguageMetadata>> DistributeLanguages(IEnumerable<Lazy<CodeRefactoringProvider, CodeChangeProviderMetadata>> providers)
45+
private static IEnumerable<Lazy<CodeRefactoringProvider, OrderableLanguageDocumentMetadata>> DistributeLanguagesAndDocuments(IEnumerable<Lazy<CodeRefactoringProvider, CodeChangeProviderMetadata>> providers)
4646
{
4747
foreach (var provider in providers)
4848
{
4949
foreach (var language in provider.Metadata.Languages)
5050
{
51-
var orderable = new OrderableLanguageMetadata(
52-
provider.Metadata.Name, language, provider.Metadata.AfterTyped, provider.Metadata.BeforeTyped);
53-
yield return new Lazy<CodeRefactoringProvider, OrderableLanguageMetadata>(() => provider.Value, orderable);
51+
foreach (var documentKind in provider.Metadata.DocumentKinds)
52+
{
53+
// Document kinds come from ExportCodeRefactoringProviderAttribute which throws
54+
// if values do not match enum TextDocumentKind. Here we'll throw too.
55+
var kind = (TextDocumentKind)Enum.Parse(typeof(TextDocumentKind), documentKind, ignoreCase: true);
56+
var documentExtensions = provider.Metadata.DocumentExtensions.Where(e => !string.IsNullOrEmpty(e));
57+
if (!documentExtensions.Any())
58+
{
59+
// Metadata without file extension applies to all files.
60+
documentExtensions = [""];
61+
}
62+
63+
foreach (var documentExtension in documentExtensions)
64+
{
65+
var orderable = new OrderableLanguageDocumentMetadata(
66+
provider.Metadata.Name ?? "", language, kind, documentExtension, provider.Metadata.AfterTyped, provider.Metadata.BeforeTyped);
67+
yield return new Lazy<CodeRefactoringProvider, OrderableLanguageDocumentMetadata>(() => provider.Value, orderable);
68+
}
69+
}
5470
}
5571
}
5672
}
5773

58-
private ImmutableDictionary<string, Lazy<ImmutableArray<CodeRefactoringProvider>>> LanguageToProvidersMap
59-
=> _lazyLanguageToProvidersMap.Value;
74+
private ImmutableDictionary<ProviderKey, Lazy<ImmutableArray<CodeRefactoringProvider>>> LanguageDocumentToProvidersMap
75+
=> _lazyLanguageDocumentToProvidersMap.Value;
6076

6177
private ImmutableDictionary<CodeRefactoringProvider, CodeChangeProviderMetadata> RefactoringToMetadataMap
6278
=> _lazyRefactoringToMetadataMap.Value;
6379

64-
private ConcatImmutableArray<CodeRefactoringProvider> GetProviders(TextDocument document)
80+
internal ConcatImmutableArray<CodeRefactoringProvider> GetProviders(TextDocument document)
6581
{
6682
var allRefactorings = ImmutableArray<CodeRefactoringProvider>.Empty;
67-
if (LanguageToProvidersMap.TryGetValue(document.Project.Language, out var lazyProviders))
83+
84+
// Include providers which apply to all extensions
85+
var key = new ProviderKey(document.Project.Language, document.Kind, "");
86+
if (LanguageDocumentToProvidersMap.TryGetValue(key, out var lazyProviders))
87+
{
88+
allRefactorings = lazyProviders.Value;
89+
}
90+
91+
// Get providers for specific combination of language, doc kind and extension,
92+
// e.g. (C#, AdditionalDocument, .xaml)
93+
if (FileNameUtilities.GetExtension(document.FilePath) is string documentExtension && documentExtension.Length > 0)
6894
{
69-
allRefactorings = ProjectCodeRefactoringProvider.FilterExtensions(document, lazyProviders.Value, GetExtensionInfo);
95+
key = new ProviderKey(document.Project.Language, document.Kind, documentExtension);
96+
if (LanguageDocumentToProvidersMap.TryGetValue(key, out lazyProviders))
97+
{
98+
allRefactorings = allRefactorings.Concat(lazyProviders.Value);
99+
}
70100
}
71101

72102
return allRefactorings.ConcatFast(GetProjectRefactorings(document));
@@ -271,4 +301,24 @@ protected override bool TryGetExtensionsFromReference(AnalyzerReference referenc
271301
return false;
272302
}
273303
}
304+
305+
private record struct ProviderKey(string Language, TextDocumentKind DocumentKind, string DocumentExtension) : IEquatable<ProviderKey>
306+
{
307+
public bool Equals(ProviderKey other)
308+
{
309+
// We create keys from two sources:
310+
// * MEF's ExportCodeRefactoringProviderAttribute when building the map for available providers.
311+
// * TextDocument when looking up the map for available providers.
312+
// Text documents can point to files with different extensions, e.g. MyPage.xaml and MyControl.XAML.
313+
// Thus we need case insensitive comparison for DocumentExtension.
314+
return Language == other.Language &&
315+
DocumentKind == other.DocumentKind &&
316+
StringComparer.OrdinalIgnoreCase.Equals(DocumentExtension, other.DocumentExtension);
317+
}
318+
319+
public override int GetHashCode()
320+
{
321+
return (Language, DocumentKind, StringComparer.OrdinalIgnoreCase.GetHashCode(DocumentExtension)).GetHashCode();
322+
}
323+
}
274324
}

src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Workspace/Mef/CodeChangeProviderMetadata.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,22 @@ namespace Microsoft.CodeAnalysis.Host.Mef;
1212
internal sealed class CodeChangeProviderMetadata : OrderableMetadata, ILanguagesMetadata
1313
{
1414
public IEnumerable<string> Languages { get; }
15+
public IEnumerable<string> DocumentKinds { get; }
16+
public IEnumerable<string> DocumentExtensions { get; }
1517

1618
public CodeChangeProviderMetadata(IDictionary<string, object> data)
1719
: base(data)
1820
{
1921
this.Languages = ((IReadOnlyDictionary<string, object>)data).GetEnumerableMetadata<string>("Languages");
22+
this.DocumentKinds = ((IReadOnlyDictionary<string, object>)data).GetEnumerableMetadata<string>("DocumentKinds");
23+
this.DocumentExtensions = ((IReadOnlyDictionary<string, object>)data).GetEnumerableMetadata<string>("DocumentExtensions");
2024
}
2125

2226
public CodeChangeProviderMetadata(string name, IEnumerable<string> after = null, IEnumerable<string> before = null, params string[] languages)
2327
: base(name, after, before)
2428
{
2529
this.Languages = languages;
30+
this.DocumentKinds = [];
31+
this.DocumentExtensions = [];
2632
}
2733
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
// See the LICENSE file in the project root for more information.
4+
5+
#nullable disable
6+
7+
using System.Collections.Generic;
8+
9+
namespace Microsoft.CodeAnalysis.Host.Mef;
10+
11+
internal sealed class OrderableLanguageDocumentMetadata : OrderableLanguageMetadata
12+
{
13+
public TextDocumentKind DocumentKind { get; }
14+
public string DocumentExtension { get; }
15+
16+
public OrderableLanguageDocumentMetadata(string name, string language, TextDocumentKind documentKind, string documentExtension, IEnumerable<string> after, IEnumerable<string> before)
17+
: base(name, language, after, before)
18+
{
19+
DocumentKind = documentKind;
20+
DocumentExtension = documentExtension;
21+
}
22+
}

src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/WorkspaceExtensions.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
<Compile Include="$(MSBuildThisFileDirectory)Workspace\Mef\MefLanguageServices.cs" />
170170
<Compile Include="$(MSBuildThisFileDirectory)Workspace\Mef\MefUtilities.cs" />
171171
<Compile Include="$(MSBuildThisFileDirectory)Workspace\Mef\MefWorkspaceServices.cs" />
172+
<Compile Include="$(MSBuildThisFileDirectory)Workspace\Mef\OrderableLanguageDocumentMetadata.cs" />
172173
<Compile Include="$(MSBuildThisFileDirectory)Workspace\Mef\OrderableLanguageMetadata.cs" />
173174
<Compile Include="$(MSBuildThisFileDirectory)Workspace\Mef\OrderableMetadata.cs" />
174175
<Compile Include="$(MSBuildThisFileDirectory)Workspace\Mef\WorkspaceServiceMetadata.cs" />

0 commit comments

Comments
 (0)