@@ -30,43 +30,73 @@ namespace Microsoft.CodeAnalysis.CodeRefactorings;
3030internal 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}
0 commit comments