/// <summary> /// This creates an instance of the WPF spell checker for the given text box /// </summary> /// <param name="textBox">The text box to initialize</param> private static void AddWpfSpellChecker(TextBox textBox) { // Don't do anything if the default spell checker is enabled or it's read-only if (!textBox.SpellCheck.IsEnabled && !textBox.IsReadOnly) { lock (syncRoot) { // Create the shared configuration and dictionary on first use if (configuration == null) { configuration = new SpellCheckerConfiguration(); configuration.Load(SpellingConfigurationFile.GlobalConfigurationFilename); var globalDictionaries = configuration.DictionaryLanguages.Select(l => GlobalDictionary.CreateGlobalDictionary(l, configuration.AdditionalDictionaryFolders, configuration.RecognizedWords)).Where(d => d != null).Distinct().ToList(); dictionary = new SpellingDictionary(globalDictionaries, configuration.IgnoredWords); } // Ignore it if disabled or it's an excluded text box string name = ElementName(textBox); if (!configuration.EnableWpfTextBoxSpellChecking || configuration.VisualStudioExclusions.Any( v => v.IsMatch(name))) { return; } } var wsc = new WpfTextBoxSpellChecker(textBox); wpfSpellCheckers.AddOrUpdate(textBox, wsc, (k, v) => wsc); } }
//===================================================================== /// <summary> /// Constructor for SpellDictionarySmartTagAction. /// </summary> /// <param name="word">The word to add or ignore.</param> /// <param name="dictionary">The dictionary used to ignore the word or add the word.</param> /// <param name="displayText">Text to show in the context menu for this action.</param> /// <param name="action">The action to take.</param> public SpellDictionarySmartTagAction(ITrackingSpan span, SpellingDictionary dictionary, string displayText, DictionaryAction action) { this.span = span; this.dictionary = dictionary; this.action = action; this.DisplayText = displayText; }
/// <summary>Constructor for multi-language spelling suggestions smart tag actions.</summary> /// <param name="trackingSpan">The tracking span.</param> /// <param name="replaceWith">The suggestion to replace misspelled word with</param> /// <param name="cultures">The cultures from which the suggested word was chosen.</param> /// <param name="dictionary">The dictionary used to perform the Replace All action</param> public MultiLanguageSpellSmartTagAction(ITrackingSpan trackingSpan, SpellingSuggestion replaceWith, IEnumerable <CultureInfo> cultures, SpellingDictionary dictionary) : base(trackingSpan, replaceWith, dictionary) { this.cultures = cultures.ToArray(); this.displayText = null; }
/// <summary>Constructor for multi-language spelling suggestions smart tag actions.</summary> /// <param name="trackingSpan">The tracking span.</param> /// <param name="replaceWith">The suggestion to replace misspelled word with</param> /// <param name="cultures">The cultures from which the suggested word was chosen.</param> /// <param name="dictionary">The dictionary used to perform the Replace All action</param> public MultiLanguageSpellSmartTagAction(ITrackingSpan trackingSpan, SpellingSuggestion replaceWith, IEnumerable<CultureInfo> cultures, SpellingDictionary dictionary) : base(trackingSpan, replaceWith, dictionary) { this.cultures = cultures.ToArray(); this.displayText = null; }
/// <summary> /// This is called to clear the textbox cache /// </summary> /// <remarks>This is done whenever a change in solution is detected. Dictionaries are cleared when that /// occurs so this will allow the text boxes to be reconnected with a valid dictionary afterwards. The /// text box instances tend to accumulate so it's also a good time to clear them all out.</remarks> public static void ClearCache() { lock (syncRoot) { configuration = null; dictionary = null; } foreach (var wsc in wpfSpellCheckers.Values.ToArray()) { wsc.Disconnect(); } }
//===================================================================== /// <summary> /// Constructor /// </summary> /// <param name="dictionary">The spelling dictionary</param> /// <param name="misspellingAggregator">The misspelling aggregator</param> public SpellSuggestedActionSource(SpellingDictionary dictionary, ITagAggregator <MisspellingTag> misspellingAggregator) { this.dictionary = dictionary; this.misspellingAggregator = misspellingAggregator; this.misspellingAggregator.TagsChanged += (sender, args) => { if (!this.disposed) { this.SuggestedActionsChanged?.Invoke(this, EventArgs.Empty); } }; }
//===================================================================== /// <summary> /// Constructor for SpellDictionarySmartTagAction. /// </summary> /// <param name="word">The word to add or ignore.</param> /// <param name="dictionary">The dictionary used to ignore the word or add the word.</param> /// <param name="displayText">Text to show in the context menu for this action.</param> /// <param name="action">The action to take.</param> /// <param name="culture">The culture of the dictionary on which to perform the action or null if it /// does not apply.</param> public SpellDictionarySmartTagAction(ITrackingSpan span, SpellingDictionary dictionary, string displayText, DictionaryAction action, CultureInfo culture) { this.span = span; this.dictionary = dictionary; this.action = action; this.culture = culture; if(culture == null) this.DisplayText = displayText; else this.DisplayText = String.Format(CultureInfo.InvariantCulture, "{0} - {1} ({2})", displayText, culture.EnglishName, culture.Name); }
//===================================================================== /// <summary> /// Constructor /// </summary> /// <param name="buffer">The text buffer</param> /// <param name="dictionary">The spelling dictionary</param> /// <param name="misspellingAggregator">The misspelling aggregator</param> public SpellSmartTagger(ITextBuffer buffer, SpellingDictionary dictionary, ITagAggregator <MisspellingTag> misspellingAggregator) { _buffer = buffer; _dictionary = dictionary; _misspellingAggregator = misspellingAggregator; _misspellingAggregator.TagsChanged += (sender, args) => { foreach (var span in args.Span.GetSpans(_buffer)) { RaiseTagsChangedEvent(span); } }; }
//===================================================================== /// <summary> /// Constructor /// </summary> /// <param name="span">The word span to replace</param> /// <param name="replaceWith">The suggestion to replace the misspelled word</param> /// <param name="dictionary">The dictionary used to perform the Replace All action</param> public SpellSuggestedAction(ITrackingSpan span, ISpellingSuggestion replaceWith, SpellingDictionary dictionary) : base(replaceWith.Suggestion.Replace("_", "__"), span) { this.replaceWith = replaceWith; this.dictionary = dictionary; // The preview is used to remind users that they can hold Ctrl when selecting this suggestion to // replace all instances of the word. this.Preview = new TextBlock(new Run { Text = "Hold Ctrl to replace all" }) { Padding = new Thickness(5) }; }
//===================================================================== /// <summary> /// Constructor for SpellDictionarySmartTagAction. /// </summary> /// <param name="word">The word to add or ignore.</param> /// <param name="dictionary">The dictionary used to ignore the word or add the word.</param> /// <param name="displayText">Text to show in the context menu for this action.</param> /// <param name="action">The action to take.</param> /// <param name="culture">The culture of the dictionary on which to perform the action or null if it /// does not apply.</param> public SpellDictionarySmartTagAction(ITrackingSpan span, SpellingDictionary dictionary, string displayText, DictionaryAction action, CultureInfo culture) { this.span = span; this.dictionary = dictionary; this.action = action; this.culture = culture; if (culture == null) { this.DisplayText = displayText; } else { this.DisplayText = String.Format(CultureInfo.InvariantCulture, "{0} - {1} ({2})", displayText, culture.EnglishName, culture.Name); } }
//===================================================================== /// <summary> /// Constructor for SpellDictionarySmartTagAction. /// </summary> /// <param name="span">The span containing the word to add or ignore.</param> /// <param name="dictionary">The dictionary used to ignore the word or add the word.</param> /// <param name="displayText">The display text for the suggested action.</param> /// <param name="action">The dictionary action to take.</param> /// <param name="culture">The culture of the dictionary on which to perform the action or null if it /// does not apply.</param> public SpellDictionarySuggestedAction(ITrackingSpan span, SpellingDictionary dictionary, string displayText, DictionaryAction action, CultureInfo culture) : base(displayText, span) { this.dictionary = dictionary; this.action = action; this.culture = culture; if (culture != null) { this.DisplayTextSuffix = $"{culture.EnglishName} ({culture.Name})"; } if (String.IsNullOrEmpty(this.DisplayText)) { this.DisplayText = !String.IsNullOrWhiteSpace(this.DisplayTextSuffix) ? this.DisplayTextSuffix : "Add to Dictionary"; this.DisplayTextSuffix = null; } }
//===================================================================== /// <summary> /// Constructor /// </summary> /// <param name="span">The span containing the word to ignore.</param> /// <param name="dictionary">The dictionary used to ignore the word or add the word.</param> /// <param name="configurationType">The configuration file type associated with the ignored words file.</param> /// <param name="ignoredWordsFile">The ignored words file to which the word is added</param> /// <param name="displayText">The display text for the suggested action.</param> public IgnoredWordsSuggestedAction(ITrackingSpan span, SpellingDictionary dictionary, ConfigurationType configurationType, string ignoredWordsFile, string displayText) : base(displayText, span) { this.dictionary = dictionary; this.ignoredWordsFile = ignoredWordsFile; if (configurationType == ConfigurationType.Global) { this.DisplayTextSuffix = "Global"; } else { this.DisplayTextSuffix = $"{configurationType} ({Path.GetFileName(ignoredWordsFile)})"; } if (String.IsNullOrWhiteSpace(this.DisplayText)) { this.DisplayText = this.DisplayTextSuffix; this.DisplayTextSuffix = null; } }
//===================================================================== /// <summary> /// Constructor for spelling suggestions smart tag actions /// </summary> /// <param name="span">The word span to replace</param> /// <param name="replaceWith">The suggestion to replace misspelled word with</param> /// <param name="dictionary">The dictionary used to perform the Replace All action</param> public SpellSmartTagAction(ITrackingSpan span, ISpellingSuggestion replaceWith, SpellingDictionary dictionary) { this.span = span; this.replaceWith = replaceWith; this.dictionary = dictionary; }
//===================================================================== /// <summary> /// Constructor /// </summary> /// <param name="trackingSpan">The tracking span</param> /// <param name="replaceWith">The suggestion to replace the misspelled word</param> /// <param name="escapeApostrophes">True to escape apostrophes in the suggestion, false if not</param> /// <param name="cultures">The cultures from which the suggested word was chosen</param> /// <param name="dictionary">The dictionary used to perform the Replace All action</param> public MultiLanguageSpellSuggestedAction(ITrackingSpan trackingSpan, ISpellingSuggestion replaceWith, bool escapeApostrophes, IEnumerable <CultureInfo> cultures, SpellingDictionary dictionary) : base(trackingSpan, replaceWith, escapeApostrophes, dictionary) { this.DisplayTextSuffix = String.Join(" | ", cultures.Select(c => c.Name)); }
/// <summary> /// Get misspelled words in the given set of spans /// </summary> /// <param name="dictionary">The dictionary to use for checking words</param> /// <param name="spans">An enumerable list of spans to check</param> /// <returns>An enumerable list of misspelling issues</returns> private IEnumerable<FileMisspelling> GetMisspellingsInSpans(SpellingDictionary dictionary, IEnumerable<SpellCheckSpan> spans) { List<Match> rangeExclusions = null; IList<string> spellingAlternates; Span errorSpan, deleteWordSpan, lastWord; string textToSplit, textToCheck, preferredTerm; // ************************************************************************************************** // NOTE: If anything changes here, update the related tagger spell checking code in // SpellingTagger.cs\GetMisspellingsInSpans(). // ************************************************************************************************** foreach(var span in spans) { cancellationTokenSource.Token.ThrowIfCancellationRequested(); textToSplit = span.Text; // Always ignore URLs rangeExclusions = WordSplitter.Url.Matches(textToSplit).OfType<Match>().ToList(); // Note the location of all XML elements if needed if(wordSplitter.Configuration.IgnoreXmlElementsInText) rangeExclusions.AddRange(WordSplitter.XmlElement.Matches(textToSplit).OfType<Match>()); // Add exclusions from the configuration if any foreach(var exclude in wordSplitter.Configuration.ExclusionExpressions) try { rangeExclusions.AddRange(exclude.Matches(textToSplit).OfType<Match>()); } catch(RegexMatchTimeoutException ex) { // Ignore expression timeouts System.Diagnostics.Debug.WriteLine(ex); } lastWord = new Span(); wordSplitter.Classification = span.Classification; foreach(var word in wordSplitter.GetWordsInText(textToSplit)) { textToCheck = textToSplit.Substring(word.Start, word.Length); // Spell check the word if it looks like one and is not ignored if(wordSplitter.IsProbablyARealWord(textToCheck) && (rangeExclusions.Count == 0 || !rangeExclusions.Any(match => word.Start >= match.Index && word.Start <= match.Index + match.Length - 1))) { errorSpan = new Span(span.Span.Start + word.Start, word.Length); // Check for a doubled word if(wordSplitter.Configuration.DetectDoubledWords && lastWord.Length != 0 && textToSplit.Substring(lastWord.Start, lastWord.Length).Equals(textToCheck, StringComparison.OrdinalIgnoreCase) && String.IsNullOrWhiteSpace(textToSplit.Substring( lastWord.Start + lastWord.Length, word.Start - lastWord.Start - lastWord.Length))) { // Delete the whitespace ahead of it too deleteWordSpan = new Span(span.Span.Start + lastWord.Start + lastWord.Length, word.Length + word.Start - lastWord.Start - lastWord.Length); yield return new FileMisspelling(errorSpan, deleteWordSpan, textToCheck); lastWord = word; continue; } lastWord = word; // If the word is not being ignored, perform the other checks if(!dictionary.ShouldIgnoreWord(textToCheck)) { // Handle code analysis dictionary checks first as they may be not be recognized as // correctly spelled words but have alternate handling. if(wordSplitter.Configuration.CadOptions.TreatDeprecatedTermsAsMisspelled && wordSplitter.Configuration.DeprecatedTerms.TryGetValue(textToCheck, out preferredTerm)) { yield return new FileMisspelling(MisspellingType.DeprecatedTerm, errorSpan, textToCheck, new[] { new SpellingSuggestion(null, preferredTerm) }); continue; } if(wordSplitter.Configuration.CadOptions.TreatCompoundTermsAsMisspelled && wordSplitter.Configuration.CompoundTerms.TryGetValue(textToCheck, out preferredTerm)) { yield return new FileMisspelling(MisspellingType.CompoundTerm, errorSpan, textToCheck, new[] { new SpellingSuggestion(null, preferredTerm) }); continue; } if(wordSplitter.Configuration.CadOptions.TreatUnrecognizedWordsAsMisspelled && wordSplitter.Configuration.UnrecognizedWords.TryGetValue(textToCheck, out spellingAlternates)) { yield return new FileMisspelling(MisspellingType.UnrecognizedWord, errorSpan, textToCheck, spellingAlternates.Select(a => new SpellingSuggestion(null, a))); continue; } if(!dictionary.IsSpelledCorrectly(textToCheck)) { // Sometimes it flags a word as misspelled if it ends with "'s". Try checking the // word without the "'s". If ignored or correct without it, don't flag it. This // appears to be caused by the definitions in the dictionary rather than Hunspell. if(textToCheck.EndsWith("'s", StringComparison.OrdinalIgnoreCase)) { textToCheck = textToCheck.Substring(0, textToCheck.Length - 2); if(dictionary.ShouldIgnoreWord(textToCheck) || dictionary.IsSpelledCorrectly(textToCheck)) continue; textToCheck += "'s"; } // Some dictionaries include a trailing period on certain words such as "etc." which // we don't include. If the word is followed by a period, try it with the period to // see if we get a match. If so, consider it valid. if(word.Start + word.Length < textToSplit.Length && textToSplit[word.Start + word.Length] == '.') { if(dictionary.ShouldIgnoreWord(textToCheck + ".") || dictionary.IsSpelledCorrectly(textToCheck + ".")) continue; } yield return new FileMisspelling(errorSpan, textToCheck); } } } } } }
//===================================================================== /// <summary> /// This is used to handle spell checking the given set of files in the background /// </summary> /// <param name="maxIssues">The maximum number of issues to report.</param> /// <param name="spellCheckFiles">The files to spell check.</param> /// <param name="codeAnalysisFiles">The code analysis dictionaries from each project that may be used in /// the configurations used for spell checking.</param> /// <param name="openDocuments">A list of documents open in the IDE. For these files, we'll get the /// content from the editor if possible rather than the file on disk.</param> private IEnumerable<FileMisspelling> SpellCheckFiles(int maxIssues, IEnumerable<SpellCheckFileInfo> spellCheckFiles, Dictionary<string, List<string>> codeAnalysisFiles, HashSet<string> openDocuments) { BindingList<FileMisspelling> issues = new BindingList<FileMisspelling>(); TextClassifier classifier; List<string> cadFiles; string documentText; try { if(wordSplitter == null) wordSplitter = new WordSplitter(); foreach(var file in spellCheckFiles) { cancellationTokenSource.Token.ThrowIfCancellationRequested(); // Get the code analysis files for the related project. These may be used to generate the // spell checking configuration. if(!codeAnalysisFiles.TryGetValue(file.ProjectFile, out cadFiles)) cadFiles = null; wordSplitter.Configuration = file.GenerateConfiguration(cadFiles); if(wordSplitter.Configuration != null) { // Create a dictionary for each configuration dictionary language ignoring any that are // invalid and duplicates caused by missing languages which return the en-US dictionary. var globalDictionaries = wordSplitter.Configuration.DictionaryLanguages.Select(l => GlobalDictionary.CreateGlobalDictionary(l, null, wordSplitter.Configuration.AdditionalDictionaryFolders, wordSplitter.Configuration.RecognizedWords)).Where(d => d != null).Distinct().ToList(); if(globalDictionaries.Any()) { var dictionary = new SpellingDictionary(globalDictionaries, wordSplitter.Configuration.IgnoredWords); classifier = ClassifierFactory.GetClassifier(file.CanonicalName, wordSplitter.Configuration); // If null, the file type is ignored if(classifier != null) { // If open in an editor, use the current text from it if possible if(openDocuments.Contains(file.CanonicalName)) { documentText = GetDocumentText(file.CanonicalName); if(documentText != null) classifier.SetText(documentText); } // Switch to the UI thread to update the progress and then switch back to this one ThreadHelper.Generic.Invoke(() => { lblProgress.Text = "Spell checking " + file.Description; }); foreach(var issue in this.GetMisspellingsInSpans(dictionary, classifier.Parse())) { issue.Dictionary = dictionary; issue.ProjectName = Path.GetFileName(file.ProjectFile); issue.Filename = file.Filename; issue.CanonicalName = file.CanonicalName; issue.LineNumber = classifier.GetLineNumber(issue.Span.Start); issue.LineText = classifier[issue.LineNumber].Trim(); issues.Add(issue); if(issues.Count >= maxIssues) break; } } } } if(issues.Count >= maxIssues) break; } } catch(OperationCanceledException) { System.Diagnostics.Debug.WriteLine("Spell checking process canceled"); } finally { wordSplitter.Configuration = null; } return issues; }