protected override void GetClassificationSpans(List <ClassificationSpan> result, SnapshotSpan span, Options options) { bool showFileNames = options.HighlightFindResultsFileNames; bool showMatches = options.HighlightFindResultsMatches; bool showDetails = options.HighlightFindResultsDetails; if (showFileNames || showMatches || showDetails) { foreach (ITextSnapshotLine line in GetSpanLines(span)) { string text = line.GetText(); if (!string.IsNullOrEmpty(text)) { // The first line in the window always contains the Find arguments, so we parse and highlight it specially. bool firstLine = line.LineNumber == 0; if (firstLine) { this.findArgs = FindArgs.TryParse(text); } // If we couldn't parse the find args (on this or an earlier call), then we don't need to iterate through the rest of the lines. if (this.findArgs == null) { break; } if (firstLine) { if (showMatches) { AddClassificationSpan(result, line, this.findArgs.PatternIndex, this.findArgs.PatternLength, matchType); } } else { this.HighlightResultLine(result, line, text, showFileNames, showMatches, showDetails); } } } } }
public static FindArgs TryParse(string text) { FindArgs result = null; // Example line to parse with all Find args enabled: // Find all "X", Match case, Whole word, Regular expressions, Subfolders, Keep modified files open, List filenames only, Find Results 1, "C:\... const string FindLinePrefix = "Find all \""; const string ReplaceLinePrefix = "Replace all \""; string linePrefix = text.StartsWith(FindLinePrefix) ? FindLinePrefix : text.StartsWith(ReplaceLinePrefix) ? ReplaceLinePrefix : null; if (!string.IsNullOrEmpty(linePrefix)) { // The Find Results header line contains an unescaped search term/expression, which can be a problem // if it contains commas, double quotes, or text that also appears as one of the options. To try to make // parsing as reliable as possible, we'll validate the line start, and then we'll work backward through the // known arg terms until we find the earliest one present. Then the unescaped search term/expression // should be in double quotes immediately before that arg. int findResults = text.LastIndexOf(", Find Results "); // This should always be present. int listFileNamesOnly = text.LastIndexOf(", List filenames only, "); int keepOpen = text.LastIndexOf(", Keep modified files open, "); // This is always present for Find; it's optional for Replace. int subfolders = text.LastIndexOf(", Subfolders, "); int regularExpressions = text.LastIndexOf(", Regular expressions, "); int wholeWord = text.LastIndexOf(", Whole word, "); int matchCase = text.LastIndexOf(", Match case, "); int block = text.LastIndexOf(", Block, "); // When "Current Block (...)" is selected int[] afterPatternChoices = new[] { findResults, listFileNamesOnly, keepOpen, subfolders, regularExpressions, wholeWord, matchCase, block, int.MaxValue // Include at least one value that always >= 0 since Min() requires that. }; int afterPattern = afterPatternChoices.Where(index => index >= 0).Min(); // VS won't let you search for an empty string, and the pattern should always have a double quote added after it. int patternIndex = linePrefix.Length; if (afterPattern < int.MaxValue && afterPattern > (patternIndex + 1)) { string pattern = text.Substring(patternIndex, afterPattern - (patternIndex + 1)); // For Replace All, the Find and Replace terms are both listed. We only want the Replace term // since it's all that we'll be able to highlight in the matched/replaced lines that are returned. if (linePrefix == ReplaceLinePrefix) { // The Find and Replace terms are unescaped, so it's possible that one contains this separator. // It's unlikely, but if it happens, then our highlights may be off or non-existent. const string Separator = "\", \""; int separatorIndex = pattern.IndexOf(Separator); if (separatorIndex >= 0) { separatorIndex += Separator.Length; pattern = pattern.Substring(separatorIndex); patternIndex += separatorIndex; } else { // Something is wrong. The Find and Replace terms weren't formatted like we expected. pattern = null; } } if (!string.IsNullOrEmpty(pattern)) { result = new FindArgs(); result.ListFileNamesOnly = listFileNamesOnly >= 0; result.PatternIndex = patternIndex; result.PatternLength = pattern.Length; try { if (regularExpressions < 0) { pattern = Regex.Escape(pattern); // VS seems to only apply the "Whole word" option when "Regular expressions" isn't used, so // we'll do the same. Otherwise, VS would return lines that we couldn't highlight a match in! if (wholeWord >= 0) { const string WholeWordBoundary = @"\b"; pattern = WholeWordBoundary + pattern + WholeWordBoundary; } } // We don't want to spend too much time searching each line. TimeSpan timeout = TimeSpan.FromMilliseconds(100); result.MatchExpression = new Regex(pattern, matchCase >= 0 ? RegexOptions.None : RegexOptions.IgnoreCase, timeout); } catch (ArgumentException) { // We did our best to parse out the pattern and build a suitable regex. But it's possible that the // parsed pattern was wrong (e.g., if it contained an unescaped ", " substring). So if Regex // throws an ArgumentException, we just can't highlight this time. result = null; } } } } return(result); }
public static FindArgs TryParse(string text) { FindArgs result = null; // VS 2019 16.5 totally changed the Find Results window and options. Update 16.5.4 restored some functionality to its List View, // but now it truncates the pattern after 20 characters. It still doesn't escape patterns, so comma and double quote are ambiguous. Match match = FindAllPattern.Match(text); if (!match.Success) { match = ReplaceAllPattern.Match(text); } if (match.Success && match.Groups.Count == 2) { int afterMatchIndex = match.Index + match.Value.Length; int listFileNamesOnly = text.IndexOf("List filenames only", afterMatchIndex, StringComparison.OrdinalIgnoreCase); int regularExpressions = text.IndexOf("Regular expressions", afterMatchIndex, StringComparison.OrdinalIgnoreCase); int wholeWord = text.IndexOf("Whole word", afterMatchIndex, StringComparison.OrdinalIgnoreCase); int matchCase = text.IndexOf("Match case", afterMatchIndex, StringComparison.OrdinalIgnoreCase); Group group = match.Groups[1]; string pattern = group.Value; const string Ellipsis = "..."; bool truncated = false; if (pattern.EndsWith(Ellipsis)) { pattern = pattern.Substring(0, pattern.Length - Ellipsis.Length); truncated = true; } result = new FindArgs { ListFileNamesOnly = listFileNamesOnly >= 0, PatternIndex = group.Index, PatternLength = pattern.Length, }; try { if (regularExpressions < 0) { pattern = Regex.Escape(pattern); // VS seems to only apply the "Whole word" option when "Regular expressions" isn't used, so we'll do the same. // We can't apply it at the end of a truncated pattern because the truncation might have occurred mid-word. if (wholeWord >= 0) { const string WholeWordBoundary = @"\b"; pattern = WholeWordBoundary + pattern + (truncated ? string.Empty : WholeWordBoundary); } } // We don't want to spend too much time searching each line. TimeSpan timeout = TimeSpan.FromMilliseconds(100); result.MatchExpression = new Regex(pattern, matchCase >= 0 ? RegexOptions.None : RegexOptions.IgnoreCase, timeout); } catch (ArgumentException) { // We did our best to parse out the pattern and build a suitable regex. But it's possible that the // parsed pattern was wrong (e.g., if it contained an unescaped ", " substring). So if Regex // throws an ArgumentException, we just can't highlight this time. result = null; } } return(result); }