22// System : Visual Studio Spell Checker Package
33// File : SpellCheckCodeFixProvider.cs
44// Author : Eric Woodruff (Eric@EWoodruff.us)
5- // Updated : 10/27 /2023
5+ // Updated : 12/29 /2023
66// Note : Copyright 2023, Eric Woodruff, All rights reserved
77//
88// This file contains a class used to provide the spell check code fixes
1717// 01/29/2023 EFW Created the code
1818//===============================================================================================================
1919
20+ // Ignore Spelling: welldone
21+
2022using System ;
23+ using System . Collections . Generic ;
2124using System . Collections . Immutable ;
2225using System . Composition ;
26+ using System . Globalization ;
2327using System . IO ;
2428using System . Linq ;
2529using System . Threading ;
3438using Microsoft . CodeAnalysis . Rename ;
3539
3640using VisualStudio . SpellChecker . CodeAnalyzer ;
41+ using VisualStudio . SpellChecker . Common ;
3742
3843namespace VisualStudio . SpellChecker . CodeFixes
3944{
@@ -65,6 +70,8 @@ public sealed override FixAllProvider GetFixAllProvider()
6570 public sealed override async Task RegisterCodeFixesAsync ( CodeFixContext context )
6671 {
6772 var root = await context . Document . GetSyntaxRootAsync ( context . CancellationToken ) . ConfigureAwait ( false ) ;
73+ var suggestions = new List < string > ( ) ;
74+ var separators = new [ ] { ',' } ;
6875
6976 foreach ( var diagnostic in context . Diagnostics )
7077 {
@@ -75,18 +82,45 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
7582 // Find the identifier for the diagnostic
7683 var syntaxToken = root . FindToken ( diagnosticSpan . Start ) ;
7784
78- if ( diagnostic . Properties . TryGetValue ( "Suggestions" , out string suggestions ) )
85+ suggestions . Clear ( ) ;
86+
87+ if ( ! diagnostic . Properties . TryGetValue ( "Suggestions" , out string suggestedReplacements ) )
7988 {
80- // If the misspelling is a sub-span, the prefix and suffix will contain the surrounding text
81- // used to create the full identifier.
82- _ = diagnostic . Properties . TryGetValue ( "Prefix" , out string prefix ) ;
83- _ = diagnostic . Properties . TryGetValue ( "Suffix" , out string suffix ) ;
89+ if ( diagnostic . Properties . TryGetValue ( "Languages" , out string languages ) &&
90+ diagnostic . Properties . TryGetValue ( "TextToCheck" , out suggestedReplacements ) )
91+ {
92+ // Getting suggestions is expensive so it's done here when actually needed rather
93+ // than in the code analyzer. Dictionaries should exist at this point since the
94+ // code analyzer will have created them.
95+ var globalDictionaries = languages . Split ( separators ,
96+ StringSplitOptions . RemoveEmptyEntries ) . Select ( l =>
97+ GlobalDictionary . CreateGlobalDictionary ( new CultureInfo ( l ) , null ,
98+ Enumerable . Empty < string > ( ) , false ) ) . Where ( d => d != null ) . Distinct ( ) . ToList ( ) ;
99+
100+ if ( globalDictionaries . Count != 0 )
101+ {
102+ var dictionary = new SpellingDictionary ( globalDictionaries , null ) ;
103+
104+ suggestions . AddRange ( CheckSuggestions (
105+ dictionary . SuggestCorrections ( suggestedReplacements ) . Select ( ss => ss . Suggestion ) ) ) ;
106+ }
107+ }
108+ }
109+ else
110+ suggestions . AddRange ( suggestedReplacements . Split ( separators , StringSplitOptions . RemoveEmptyEntries ) ) ;
84111
112+ if ( suggestedReplacements != null )
113+ {
85114 ImmutableArray < CodeAction > replacements ;
86115
87- if ( ! String . IsNullOrWhiteSpace ( suggestions ) )
116+ if ( suggestions . Count != 0 )
88117 {
89- replacements = suggestions . Split ( new [ ] { ',' } , StringSplitOptions . RemoveEmptyEntries ) . Select (
118+ // If the misspelling is a sub-span, the prefix and suffix will contain the surrounding text
119+ // used to create the full identifier.
120+ _ = diagnostic . Properties . TryGetValue ( "Prefix" , out string prefix ) ;
121+ _ = diagnostic . Properties . TryGetValue ( "Suffix" , out string suffix ) ;
122+
123+ replacements = suggestions . Select (
90124 s =>
91125 {
92126 string replacement = ( String . IsNullOrWhiteSpace ( prefix ) &&
@@ -124,6 +158,54 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
124158 }
125159 }
126160
161+ /// <summary>
162+ /// This is used to filter and adjust the suggestions used to fix a misspelling in an identifier
163+ /// </summary>
164+ /// <param name="suggestions">The suggestions that should replace the misspelling</param>
165+ /// <returns>An enumerable list of valid suggestions, if any.</returns>
166+ /// <remarks>Some suggestions include spaces or punctuation. Those are altered to remove the punctuation
167+ /// and return the suggestion in camel case.</remarks>
168+ private static HashSet < string > CheckSuggestions ( IEnumerable < string > suggestions )
169+ {
170+ var validSuggestions = new HashSet < string > ( ) ;
171+
172+ foreach ( string s in suggestions )
173+ {
174+ var wordChars = s . ToArray ( ) ;
175+
176+ if ( wordChars . All ( c => Char . IsLetter ( c ) ) )
177+ validSuggestions . Add ( s ) ;
178+ else
179+ {
180+ // Certain misspellings may return suggestions with spaces or punctuation. For example:
181+ // welldone suggests "well done" and "well-done". Return those as a camel case suggestion:
182+ // wellDone.
183+ bool caseChanged = false ;
184+
185+ for ( int idx = 0 ; idx < wordChars . Length ; idx ++ )
186+ {
187+ if ( ! Char . IsLetter ( wordChars [ idx ] ) )
188+ {
189+ while ( idx < wordChars . Length && ! Char . IsLetter ( wordChars [ idx ] ) )
190+ idx ++ ;
191+
192+ if ( idx < wordChars . Length )
193+ {
194+ wordChars [ idx ] = Char . ToUpperInvariant ( wordChars [ idx ] ) ;
195+ caseChanged = true ;
196+ }
197+ }
198+ }
199+
200+ if ( caseChanged )
201+ validSuggestions . Add ( new String ( wordChars . Where ( c => Char . IsLetter ( c ) ) . ToArray ( ) ) ) ;
202+ }
203+ }
204+
205+ return validSuggestions ;
206+ }
207+
208+
127209 /// <summary>
128210 /// Create the solution used to correct a spelling error
129211 /// </summary>
0 commit comments