/// <summary> /// This is overridden to initialize the package /// </summary> /// <remarks>This method is called right after the package is sited, so this is the place where you can /// put all the initialization code that relies on services provided by Visual Studio.</remarks> protected override async System.Threading.Tasks.Task InitializeAsync( System.Threading.CancellationToken cancellationToken, IProgress <ServiceProgressData> progress) { Trace.WriteLine($"Entering Initialize() of {this.ToString()}"); await base.InitializeAsync(cancellationToken, progress); // When initialized asynchronously, we *may* be on a background thread at this point. Do any // initialization that requires the UI thread after switching to the UI thread. await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); Instance = this; try { var configuration = new SpellCheckerConfiguration(); configuration.Load(SpellingConfigurationFile.GlobalConfigurationFilename); if (configuration.EnableWpfTextBoxSpellChecking) { this.ConnectSpellChecker(); } } catch (Exception ex) { // Ignore any exceptions Debug.WriteLine(ex); } }
//===================================================================== /// <summary> /// Get the configuration settings for the specified buffer /// </summary> /// <param name="buffer">The buffer for which to get the configuration settings</param> /// <returns>The spell checker configuration settings for the buffer or null if one is not provided or /// is disabled for the given buffer.</returns> public SpellCheckerConfiguration GetConfiguration(ITextBuffer buffer) { SpellCheckerConfiguration config = null; bool isDisabled = false; // If not given a buffer or already checked for and found to be disabled, don't go any further if (buffer != null && !buffer.Properties.TryGetProperty(SpellCheckerDisabledKey, out isDisabled) && !buffer.Properties.TryGetProperty(typeof(SpellCheckerConfiguration), out config)) { // Generate the configuration settings unique to the file config = this.GenerateConfiguration(buffer); if (!config.SpellCheckAsYouType || config.IsExcludedByExtension(buffer.GetFilenameExtension())) { // Mark it as disabled so that we don't have to check again buffer.Properties[SpellCheckerDisabledKey] = true; config = null; } else { buffer.Properties[typeof(SpellCheckerConfiguration)] = config; } } return(config); }
//===================================================================== /// <summary> /// Get the configuration settings for the specified buffer /// </summary> /// <param name="buffer">The buffer for which to get the configuration settings</param> /// <returns>The spell checker configuration settings for the buffer or null if one is not provided or /// is disabled for the given buffer.</returns> public SpellCheckerConfiguration GetConfiguration(ITextBuffer buffer) { SpellCheckerConfiguration config = null; // If not given a buffer or already checked for and found to be disabled, don't go any further if (buffer != null && !buffer.Properties.TryGetProperty(SpellCheckerDisabledKey, out bool isDisabled) && !buffer.Properties.TryGetProperty(typeof(SpellCheckerConfiguration), out config)) { #pragma warning disable VSTHRD010 // Generate the configuration settings unique to the file config = this.GenerateConfiguration(buffer); #pragma warning restore VSTHRD010 if (config == null || !config.SpellCheckAsYouType || config.ShouldExcludeFile(buffer.GetFilename())) { // Mark it as disabled so that we don't have to check again buffer.Properties[SpellCheckerDisabledKey] = true; config = null; } else { buffer.Properties[typeof(SpellCheckerConfiguration)] = config; } } return(config); }
//===================================================================== /// <summary> /// Creates a tag provider for the specified view and buffer /// </summary> /// <typeparam name="T">The tag type</typeparam> /// <param name="textView">The text view</param> /// <param name="buffer">The text buffer</param> /// <returns>The tag provider for the specified view and buffer or null if the buffer does not match the /// one in the view or spell checking as you type is disabled.</returns> public ITagger <T> CreateTagger <T>(ITextView textView, ITextBuffer buffer) where T : ITag { SpellingTagger spellingTagger; // Make sure we only tagging top buffer and only if wanted if (textView.TextBuffer != buffer || !SpellCheckerConfiguration.SpellCheckAsYouType || SpellCheckerConfiguration.IsExcludedByExtension(buffer.GetFilenameExtension())) { return(null); } if (textView.Properties.TryGetProperty(typeof(SpellingTagger), out spellingTagger)) { return(spellingTagger as ITagger <T>); } var dictionary = spellingDictionaryFactory.GetDictionary(buffer); if (dictionary == null) { return(null); } var naturalTextAggregator = aggregatorFactory.CreateTagAggregator <INaturalTextTag>(textView, TagAggregatorOptions.MapByContentType); var urlAggregator = aggregatorFactory.CreateTagAggregator <IUrlTag>(textView); spellingTagger = new SpellingTagger(buffer, textView, naturalTextAggregator, urlAggregator, dictionary); textView.Properties[typeof(SpellingTagger)] = spellingTagger; return(spellingTagger as ITagger <T>); }
/// <summary> /// This is overridden to initialize the package /// </summary> /// <remarks>This method is called right after the package is sited, so this is the place where you can /// put all the initialization code that relies on services provided by Visual Studio.</remarks> protected override void Initialize() { Trace.WriteLine(String.Format(CultureInfo.CurrentCulture, "Entering Initialize() of {0}", this.ToString())); base.Initialize(); Instance = this; try { var configuration = new SpellCheckerConfiguration(); configuration.Load(SpellingConfigurationFile.GlobalConfigurationFilename); if (configuration.EnableWpfTextBoxSpellChecking) { this.ConnectSpellChecker(); } } catch (Exception ex) { // Ignore any exceptions Debug.WriteLine(ex); } }
//===================================================================== /// <summary> /// Constructor /// </summary> /// <param name="buffer">The text buffer</param> /// <param name="view">The text view</param> /// <param name="naturalTextAggregator">The tag aggregator</param> /// <param name="urlAggregator">The URL aggregator</param> /// <param name="configuration">The spell checker configuration to use</param> /// <param name="dictionary">The spelling dictionary to use</param> public SpellingTagger(ITextBuffer buffer, ITextView view, ITagAggregator <INaturalTextTag> naturalTextAggregator, ITagAggregator <IUrlTag> urlAggregator, SpellCheckerConfiguration configuration, SpellingDictionary dictionary) { _isClosed = false; _buffer = buffer; _naturalTextAggregator = naturalTextAggregator; _urlAggregator = urlAggregator; _dispatcher = Dispatcher.CurrentDispatcher; this.configuration = configuration; _dictionary = dictionary; _dirtySpans = new List <SnapshotSpan>(); _misspellings = new List <MisspellingTag>(); wordsIgnoredOnce = new List <IgnoredWord>(); _buffer.Changed += BufferChanged; _naturalTextAggregator.TagsChanged += AggregatorTagsChanged; _urlAggregator.TagsChanged += AggregatorTagsChanged; _dictionary.DictionaryUpdated += DictionaryUpdated; _dictionary.ReplaceAll += ReplaceAll; _dictionary.IgnoreOnce += IgnoreOnce; view.Closed += ViewClosed; // To start with, the entire buffer is dirty. Split this into chunks so we update pieces at a time. ITextSnapshot snapshot = _buffer.CurrentSnapshot; foreach (var line in snapshot.Lines) { AddDirtySpan(line.Extent); } }
/// <inheritdoc /> public bool ShouldIgnoreWord(string word) { lock (ignoredWords) { if (ignoredWords.Contains(word)) { return(true); } } return(SpellCheckerConfiguration.ShouldIgnoreWord(word)); }
//===================================================================== /// <summary> /// Constructor /// </summary> /// <param name="buffer">The text buffer</param> /// <param name="view">The text view</param> /// <param name="naturalTextAggregator">The tag aggregator</param> /// <param name="urlAggregator">The URL aggregator</param> /// <param name="configuration">The spell checker configuration to use</param> /// <param name="dictionary">The spelling dictionary to use</param> public SpellingTagger(ITextBuffer buffer, ITextView view, ITagAggregator <INaturalTextTag> naturalTextAggregator, ITagAggregator <IUrlTag> urlAggregator, SpellCheckerConfiguration configuration, SpellingDictionary dictionary) { _isClosed = false; _buffer = buffer; _naturalTextAggregator = naturalTextAggregator; _urlAggregator = urlAggregator; _dispatcher = Dispatcher.CurrentDispatcher; this.configuration = configuration; _dictionary = dictionary; _dirtySpans = new List <SnapshotSpan>(); _misspellings = new List <MisspellingTag>(); wordsIgnoredOnce = new List <IgnoredOnceWord>(); inlineIgnoredWords = new List <InlineIgnoredWord>(); string filename = buffer.GetFilename(); wordSplitter = new WordSplitter { Configuration = configuration, Mnemonic = ClassifierFactory.GetMnemonic(filename), IsCStyleCode = ClassifierFactory.IsCStyleCode(filename) }; _buffer.Changed += BufferChanged; _naturalTextAggregator.TagsChanged += AggregatorTagsChanged; _urlAggregator.TagsChanged += AggregatorTagsChanged; _dictionary.DictionaryUpdated += DictionaryUpdated; _dictionary.ReplaceAll += ReplaceAll; _dictionary.IgnoreOnce += IgnoreOnce; view.Closed += ViewClosed; // Strings in SQL script can contain escaped single quotes which are apostrophes. Unescape them // so that they are spell checked correctly. unescapeApostrophes = buffer.ContentType.IsOfType("SQL Server Tools"); // To start with, the entire buffer is dirty. Split this into chunks so we update pieces at a time. ITextSnapshot snapshot = _buffer.CurrentSnapshot; foreach (var line in snapshot.Lines) { AddDirtySpan(line.Extent); } }
//===================================================================== /// <summary> /// Generate the configuration to use when spell checking the given text buffer /// </summary> /// <param name="buffer">The text buffer for which to generate a configuration</param> /// <returns>The generated configuration to use</returns> /// <remarks>The configuration is a merger of the global settings plus any solution, project, folder, and /// file settings related to the text buffer.</remarks> private SpellCheckerConfiguration GenerateConfiguration(ITextBuffer buffer) { ProjectItem projectItem, fileItem; string bufferFilename, filename, projectPath, projectFilename = null; // Start with the global configuration var config = new SpellCheckerConfiguration(); try { config.Load(SpellingConfigurationFile.GlobalConfigurationFilename); var dte2 = (globalServiceProvider == null) ? null : globalServiceProvider.GetService(typeof(SDTE)) as DTE2; if (dte2 != null && dte2.Solution != null && !String.IsNullOrWhiteSpace(dte2.Solution.FullName)) { var solution = dte2.Solution; // Clear the global dictionary cache when a change in solution is detected. This handles // cases where only the MEF components are loaded and not the package (i.e. a configuration // has not been edited). See VSSpellCheckerPackage.solutionEvents_AfterClosing(). if (lastSolutionName == null || !lastSolutionName.Equals(solution.FullName, StringComparison.OrdinalIgnoreCase)) { GlobalDictionary.ClearDictionaryCache(); lastSolutionName = solution.FullName; } // See if there is a solution configuration filename = solution.FullName + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } // Find the project item for the file we are opening bufferFilename = buffer.GetFilename(); projectItem = (bufferFilename != null) ? solution.FindProjectItem(bufferFilename) : null; if (projectItem != null) { fileItem = projectItem; // If we have a project (we should), see if it has settings if (projectItem.ContainingProject != null && !String.IsNullOrWhiteSpace(projectItem.ContainingProject.FullName)) { projectFilename = projectItem.ContainingProject.FullName; filename = projectFilename + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } // Get the full path based on the project. The buffer filename will refer to the actual // path which may be to a linked file outside the project's folder structure. projectPath = Path.GetDirectoryName(filename); filename = Path.GetDirectoryName((string)fileItem.Properties.Item("FullPath").Value); // Search for folder-specific configuration files if (filename.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase)) { // Then check subfolders. No need to check the root folder as the project // settings cover it. if (filename.Length > projectPath.Length) { foreach (string folder in filename.Substring(projectPath.Length + 1).Split('\\')) { projectPath = Path.Combine(projectPath, folder); filename = Path.Combine(projectPath, folder + ".vsspell"); projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } } } } // If the item looks like a dependent file item, look for a settings file related to // the parent file item. if (fileItem.Collection != null && fileItem.Collection.Parent != null) { projectItem = fileItem.Collection.Parent as ProjectItem; if (projectItem != null && projectItem.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFile) { filename = (string)projectItem.Properties.Item("FullPath").Value + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } } } // And finally, look for file-specific settings for the item itself filename = (string)fileItem.Properties.Item("FullPath").Value + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } } else if (projectItem.Kind == EnvDTE.Constants.vsProjectItemKindSolutionItems) { // Looks like a solution item, see if a related setting file exists filename = bufferFilename + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } } } // Load code analysis dictionaries if wanted if (projectFilename != null && config.CadOptions.ImportCodeAnalysisDictionaries) { // I'm not sure if there's a better way to do this but it does seem to work. We need to // find one or more arbitrary files with an item type of "CodeAnalysisDictionary". We // do so by getting the MSBuild project from the global project collection and using its // GetItems() method to find them. var loadedProject = Microsoft.Build.Evaluation.ProjectCollection.GlobalProjectCollection.GetLoadedProjects( projectFilename).FirstOrDefault(); if (loadedProject != null) { // Typically there is only one but multiple files are supported foreach (var cad in loadedProject.GetItems("CodeAnalysisDictionary")) { filename = Path.Combine(Path.GetDirectoryName(projectFilename), cad.EvaluatedInclude); if (File.Exists(filename)) { config.ImportCodeAnalysisDictionary(filename); } } } } if (bufferFilename != null && config.DetermineResourceFileLanguageFromName && Path.GetExtension(bufferFilename).Equals(".resx", StringComparison.OrdinalIgnoreCase)) { // Localized resource files are expected to have filenames in the format // BaseName.Language.resx (i.e. LocalizedForm.de-DE.resx). bufferFilename = Path.GetExtension(Path.GetFileNameWithoutExtension(bufferFilename)); if (bufferFilename.Length > 1) { bufferFilename = bufferFilename.Substring(1); SpellCheckerDictionary match; if (SpellCheckerDictionary.AvailableDictionaries( config.AdditionalDictionaryFolders).TryGetValue(bufferFilename, out match)) { // Clear any existing dictionary languages and use just the one that matches the // file's language. config.DictionaryLanguages.Clear(); config.DictionaryLanguages.Add(match.Culture); } } } } else if (lastSolutionName != null) { // A solution was closed and a file has been opened outside of a solution so clear the // cache and use the global dictionaries. GlobalDictionary.ClearDictionaryCache(); lastSolutionName = null; } } catch (Exception ex) { // Ignore errors, we just won't load the configurations after the point of failure System.Diagnostics.Debug.WriteLine(ex); } return(config); }
/// <summary> /// Get all words in the specified text string /// </summary> /// <param name="text">The text to break into words</param> /// <returns>An enumerable list of word spans</returns> internal static IEnumerable <Microsoft.VisualStudio.Text.Span> GetWordsInText(string text) { if (String.IsNullOrWhiteSpace(text)) { yield break; } for (int i = 0, end = 0; i < text.Length; i++) { // Skip escape sequences. If not, they can end up as part of the word or cause words to be // missed. For example, "This\r\nis\ta\ttest \x22missing\x22" would incorrectly yield "nis", // "ta", and "ttest" and incorrectly exclude "missing". This can cause the occasional false // positive in file paths (i.e. \Folder\transform\File.txt flags "ransform" as a misspelled word // because of the lowercase "t" following the backslash) but I can live with that. If they are // common enough, they can be added to the configuration's ignored word list as an escaped word. if (text[i] == '\\') { end = i + 1; if (end < text.Length) { // Skip escaped words. Only need to check the escape sequence letters. switch (text[end]) { case 'a': // BEL case 'b': // BS case 'f': // FF case 'n': // LF case 'r': // CR case 't': // TAB case 'v': // VT case 'x': // Hex value case 'u': // Unicode value case 'U': { // Find the end of the word int wordEnd = end; while (++wordEnd < text.Length && !IsWordBreakCharacter(text[wordEnd])) { ; } if (SpellCheckerConfiguration.ShouldIgnoreWord( text.Substring(end - 1, --wordEnd - i + 1))) { i = wordEnd; continue; } break; } } // Escape sequences switch (text[end]) { case '\'': case '\"': case '\\': case '?': // Anti-Trigraph case '0': // NUL or Octal case 'a': // BEL case 'b': // BS case 'f': // FF case 'n': // LF case 'r': // CR case 't': // TAB case 'v': // VT i++; break; case 'x': // xh[h[h[h]]] or xhh[hh] while (++end < text.Length && (end - i) < 6 && (Char.IsDigit(text[end]) || (Char.ToLower(text[end]) >= 'a' && Char.ToLower(text[end]) <= 'f'))) { ; } i = --end; break; case 'u': // uhhhh while (++end < text.Length && (end - i) < 6 && (Char.IsDigit(text[end]) || (Char.ToLower(text[end]) >= 'a' && Char.ToLower(text[end]) <= 'f'))) { ; } if ((--end - i) == 5) { i = end; } break; case 'U': // Uhhhhhhhh while (++end < text.Length && (end - i) < 10 && (Char.IsDigit(text[end]) || (Char.ToLower(text[end]) >= 'a' && Char.ToLower(text[end]) <= 'f'))) { ; } if ((--end - i) == 9) { i = end; } break; default: break; } } continue; } // Skip XML entities if (text[i] == '&') { end = i + 1; if (end < text.Length && text[end] == '#') { // Numeric Reference &#n[n][n][n]; while (++end < text.Length && (end - i) < 7 && Char.IsDigit(text[end])) { ; } // Hexadecimal Reference &#xh[h][h][h]; if (end < text.Length && text[end] == 'x') { while (++end < text.Length && (end - i) < 8 && (Char.IsDigit(text[end]) || (Char.ToLower(text[end]) >= 'a' && Char.ToLower(text[end]) <= 'f'))) { ; } } // Check for entity closer if (end < text.Length && text[end] == ';') { i = end; } } continue; } // Skip .NET format string specifiers if so indicated. This ignores stuff like date formats // such as "{0:MM/dd/yyyy hh:nn tt}". if (text[i] == '{' && SpellCheckerConfiguration.IgnoreFormatSpecifiers) { end = i + 1; while (end < text.Length && Char.IsDigit(text[end])) { end++; } if (end < text.Length && text[end] == ':') { // Find the end accounting for escaped braces while (++end < text.Length) { if (text[end] == '}') { if (end + 1 == text.Length || text[end + 1] != '}') { break; } end++; } } } if (end < text.Length && text[end] == '}') { i = end; } continue; } // Skip C-style format string specifiers if so indicated. These can cause spelling errors in // cases where there are multiple characters such as "%ld". My C/C++ skills are very rusty but // this should cover it. if (text[i] == '%' && SpellCheckerConfiguration.IgnoreFormatSpecifiers) { end = i + 1; if (end < text.Length) { // Flags switch (text[end]) { // NOTE: A space is also a valid flag character but we can't tell if it's part of // the format or just a percentage followed by a word without some lookahead which // probably isn't worth the effort (i.e. "% i" vs "100% stuff"). As such, the space // flag character is not included here. case '-': case '+': case '#': case '0': end++; break; default: break; } // Width and precision not accounting for validity to keep it simple while (end < text.Length && (Char.IsDigit(text[end]) || text[end] == '.' || text[end] == '*')) { end++; } if (end < text.Length) { // Length switch (text[end]) { case 'h': case 'l': end++; // Check for "hh" and "ll" if (end < text.Length && text[end] == text[end - 1]) { end++; } break; case 'j': case 'z': case 't': case 'L': end++; break; default: break; } if (end < text.Length) { // And finally, the specifier switch (text[end]) { case 'd': case 'i': case 'u': case 'o': case 'x': case 'X': case 'f': case 'F': case 'e': case 'E': case 'g': case 'G': case 'a': case 'A': case 'c': case 's': case 'p': case 'n': i = end; break; default: break; } } } } continue; } // Skip word separator if (IsWordBreakCharacter(text[i])) { continue; } // Find the end of the word end = i; while (++end < text.Length && !IsWordBreakCharacter(text[end])) { ; } // Skip XML entity reference &[name]; if (end < text.Length && i > 0 && text[i - 1] == '&' && text[end] == ';') { i = end; continue; } // Skip leading apostrophes while (i < end && text[i] == '\'') { i++; } // Skip trailing apostrophes, periods, and at-signs while (--end > i && (text[end] == '\'' || text[end] == '.' || text[end] == '@')) { ; } end++; // Move back to last match // Ignore anything less than two characters if (end - i > 1) { yield return(Microsoft.VisualStudio.Text.Span.FromBounds(i, end)); } i = --end; } }
/// <summary> /// Generate the configuration to use when spell checking the given text buffer /// </summary> /// <param name="buffer">The text buffer for which to generate a configuration</param> /// <returns>The generated configuration to use</returns> /// <remarks>The configuration is a merger of the global settings plus any solution, project, folder, and /// file settings related to the text buffer.</remarks> private static SpellCheckerConfiguration GenerateConfiguration(ITextBuffer buffer) { Microsoft.VisualStudio.Shell.ThreadHelper.ThrowIfNotOnUIThread(); ProjectItem projectItem, fileItem; string bufferFilename, filename, projectPath, projectFilename = null; // Start with the global configuration var config = new SpellCheckerConfiguration(); try { config.Load(SpellingConfigurationFile.GlobalConfigurationFilename); if (Package.GetGlobalService(typeof(SDTE)) is DTE2 dte2 && dte2.Solution != null && !String.IsNullOrWhiteSpace(dte2.Solution.FullName)) { var solution = dte2.Solution; // Clear the global dictionary cache when a change in solution is detected. This handles // cases where only the MEF components are loaded and not the package (i.e. a configuration // has not been edited). See VSSpellCheckerPackage.solutionEvents_AfterClosing(). if (LastSolutionName == null || !LastSolutionName.Equals(solution.FullName, StringComparison.OrdinalIgnoreCase)) { WpfTextBox.WpfTextBoxSpellChecker.ClearCache(); GlobalDictionary.ClearDictionaryCache(); LastSolutionName = solution.FullName; } // See if there is a solution configuration filename = solution.FullName + ".vsspell"; projectItem = solution.FindProjectItemForFile(filename); if (projectItem != null) { config.Load(filename); } // Find the project item for the file we are opening bufferFilename = buffer.GetFilename(); projectItem = solution.FindProjectItemForFile(bufferFilename); if (projectItem != null) { fileItem = projectItem; // If we have a project (we should), see if it has settings if (projectItem.ContainingProject != null && !String.IsNullOrWhiteSpace(projectItem.ContainingProject.FullName)) { projectFilename = projectItem.ContainingProject.FullName; // Website projects are named after the folder if (projectFilename.Length > 1 && projectFilename[projectFilename.Length - 1] == '\\') { filename = Path.GetFileName(projectFilename.Substring(0, projectFilename.Length - 1)); filename = projectFilename + filename + ".vsspell"; } else { filename = projectFilename + ".vsspell"; } projectItem = solution.FindProjectItemForFile(filename); if (projectItem != null) { config.Load(filename); } // Get the full path based on the project. The buffer filename will refer to the actual // path which may be to a linked file outside the project's folder structure. projectPath = Path.GetDirectoryName(filename); filename = Path.GetDirectoryName((string)fileItem.Properties.Item("FullPath").Value); // Search for folder-specific configuration files if (filename.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase)) { // Then check subfolders. No need to check the root folder as the project // settings cover it. if (filename.Length > projectPath.Length) { foreach (string folder in filename.Substring(projectPath.Length + 1).Split('\\')) { projectPath = Path.Combine(projectPath, folder); filename = Path.Combine(projectPath, folder + ".vsspell"); projectItem = solution.FindProjectItemForFile(filename); if (projectItem != null) { config.Load(filename); } } } } // If the item looks like a dependent file item, look for a settings file related to // the parent file item. if (fileItem.Collection != null && fileItem.Collection.Parent != null) { projectItem = fileItem.Collection.Parent as ProjectItem; if (projectItem != null && projectItem.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFile) { filename = (string)projectItem.Properties.Item("FullPath").Value + ".vsspell"; projectItem = solution.FindProjectItemForFile(filename); if (projectItem != null) { config.Load(filename); } } } // And finally, look for file-specific settings for the item itself filename = (string)fileItem.Properties.Item("FullPath").Value + ".vsspell"; projectItem = solution.FindProjectItemForFile(filename); if (projectItem != null) { config.Load(filename); } } else if (projectItem.Kind == EnvDTE.Constants.vsProjectItemKindSolutionItems) { // Looks like a solution item, see if a related setting file exists filename = bufferFilename + ".vsspell"; projectItem = solution.FindProjectItemForFile(filename); if (projectItem != null) { config.Load(filename); } } } // Load code analysis dictionaries if wanted if (projectFilename != null && config.CadOptions.ImportCodeAnalysisDictionaries) { // Typically there is only one but multiple files are supported foreach (var cad in SpellCheckFileInfo.ProjectCodeAnalysisDictionaries(projectFilename)) { if (File.Exists(cad.CanonicalName)) { config.ImportCodeAnalysisDictionary(cad.CanonicalName); } } } if (bufferFilename != null && config.DetermineResourceFileLanguageFromName && Path.GetExtension(bufferFilename).Equals(".resx", StringComparison.OrdinalIgnoreCase)) { // Localized resource files are expected to have filenames in the format // BaseName.Language.resx (i.e. LocalizedForm.de-DE.resx). bufferFilename = Path.GetExtension(Path.GetFileNameWithoutExtension(bufferFilename)); if (bufferFilename.Length > 1) { bufferFilename = bufferFilename.Substring(1); if (SpellCheckerDictionary.AvailableDictionaries( config.AdditionalDictionaryFolders).TryGetValue(bufferFilename, out SpellCheckerDictionary match)) { // Clear any existing dictionary languages and use just the one that matches the // file's language. config.DictionaryLanguages.Clear(); config.DictionaryLanguages.Add(match.Culture); } } } } else if (LastSolutionName != null) { // A solution was closed and a file has been opened outside of a solution so clear the // cache and use the global dictionaries. WpfTextBox.WpfTextBoxSpellChecker.ClearCache(); GlobalDictionary.ClearDictionaryCache(); LastSolutionName = null; } }
//===================================================================== /// <summary> /// Generate the configuration to use when spell checking the given text buffer /// </summary> /// <param name="buffer">The text buffer for which to generate a configuration</param> /// <returns>The generated configuration to use</returns> /// <remarks>The configuration is a merger of the global settings plus any solution, project, folder, and /// file settings related to the text buffer.</remarks> private SpellCheckerConfiguration GenerateConfiguration(ITextBuffer buffer) { ProjectItem projectItem, fileItem; string bufferFilename, filename, projectPath; // Start with the global configuration var config = new SpellCheckerConfiguration(); try { config.Load(SpellingConfigurationFile.GlobalConfigurationFilename); var dte2 = (globalServiceProvider == null) ? null : globalServiceProvider.GetService(typeof(SDTE)) as DTE2; if (dte2 != null && dte2.Solution != null && !String.IsNullOrWhiteSpace(dte2.Solution.FullName)) { var solution = dte2.Solution; // Clear the global dictionary cache when a change in solution is detected if (lastSolutionName == null || !lastSolutionName.Equals(solution.FullName, StringComparison.OrdinalIgnoreCase)) { GlobalDictionary.ClearDictionaryCache(); lastSolutionName = solution.FullName; } // See if there is a solution configuration filename = solution.FullName + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } // Find the project item for the file we are opening bufferFilename = buffer.GetFilename(); projectItem = (bufferFilename != null) ? solution.FindProjectItem(bufferFilename) : null; if (projectItem != null) { fileItem = projectItem; // If we have a project (we should), see if it has settings if (projectItem.ContainingProject != null && !String.IsNullOrWhiteSpace(projectItem.ContainingProject.FullName)) { filename = projectItem.ContainingProject.FullName + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } // Get the full path based on the project. The buffer filename will refer to the actual // path which may be to a linked file outside the project's folder structure. projectPath = Path.GetDirectoryName(filename); filename = Path.GetDirectoryName((string)fileItem.Properties.Item("FullPath").Value); // Search for folder-specific configuration files if (filename.StartsWith(projectPath, StringComparison.OrdinalIgnoreCase)) { // Then check subfolders. No need to check the root folder as the project // settings cover it. if (filename.Length > projectPath.Length) { foreach (string folder in filename.Substring(projectPath.Length + 1).Split('\\')) { projectPath = Path.Combine(projectPath, folder); filename = Path.Combine(projectPath, folder + ".vsspell"); projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } } } } // If the item looks like a dependent file item, look for a settings file related to // the parent file item. if (fileItem.Collection != null && fileItem.Collection.Parent != null) { projectItem = fileItem.Collection.Parent as ProjectItem; if (projectItem != null && projectItem.Kind == EnvDTE.Constants.vsProjectItemKindPhysicalFile) { filename = (string)projectItem.Properties.Item("FullPath").Value + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } } } // And finally, look for file-specific settings for the item itself filename = (string)fileItem.Properties.Item("FullPath").Value + ".vsspell"; projectItem = solution.FindProjectItem(filename); if (projectItem != null) { config.Load(filename); } } } if (bufferFilename != null && config.DetermineResourceFileLanguageFromName && Path.GetExtension(bufferFilename).Equals(".resx", StringComparison.OrdinalIgnoreCase)) { // Localized resource files are expected to have filenames in the format // BaseName.Language.resx (i.e. LocalizedForm.de-DE.resx). bufferFilename = Path.GetExtension(Path.GetFileNameWithoutExtension(bufferFilename)); if (bufferFilename.Length > 1) { bufferFilename = bufferFilename.Substring(1); SpellCheckerDictionary match; if (SpellCheckerDictionary.AvailableDictionaries( config.AdditionalDictionaryFolders).TryGetValue(bufferFilename, out match)) { config.DefaultLanguage = match.Culture; } } } } else if (lastSolutionName != null) { // A solution was closed and a file has been opened outside of a solution so clear the // cache and use the global dictionaries. GlobalDictionary.ClearDictionaryCache(); lastSolutionName = null; } } catch (Exception ex) { // Ignore errors, we just won't load the configurations after the point of failure System.Diagnostics.Debug.WriteLine(ex); } return(config); }