Editor/HotCodeEditor/CodeView/TextSearch.cs
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
public readonly record struct SearchMatch( TextPos Start, TextPos End, Match RegexMatch );
public record struct SearchOptions( string Query, bool MatchCase, bool WholeWord, bool UseRegex );
/// <summary>
/// Finding text in a <see cref="TextBuffer"/>. Plain searches go through Regex too (escaped)
/// so every option behaves the same way.
/// </summary>
public static class TextSearch
{
public const int MaxMatches = 10000;
/// <summary>
/// All matches in order. Returns an empty list and sets <paramref name="error"/> for a bad regex.
/// </summary>
public static List<SearchMatch> FindAll( TextBuffer buffer, SearchOptions options, out string error )
{
error = null;
var results = new List<SearchMatch>();
if ( string.IsNullOrEmpty( options.Query ) ) return results;
var regex = Build( options, out error );
if ( regex is null ) return results;
var text = string.Join( "\n", buffer.Lines );
var lineStarts = new int[buffer.LineCount];
for ( int i = 1; i < buffer.LineCount; i++ )
lineStarts[i] = lineStarts[i - 1] + buffer.Lines[i - 1].Length + 1;
try
{
for ( var m = regex.Match( text ); m.Success && results.Count < MaxMatches; m = m.NextMatch() )
{
// Zero-length matches (like "^") can't be selected or replaced sensibly
if ( m.Length == 0 ) continue;
results.Add( new SearchMatch( ToPos( lineStarts, m.Index ), ToPos( lineStarts, m.Index + m.Length ), m ) );
}
}
catch ( RegexMatchTimeoutException )
{
error = "Search took too long";
}
return results;
}
/// <summary>
/// What a match gets replaced with. With regex on, $1 / ${name} refer to groups.
/// </summary>
public static string ReplacementFor( SearchMatch match, string replacement, SearchOptions options )
{
replacement ??= "";
return options.UseRegex ? match.RegexMatch.Result( replacement ) : replacement;
}
private static Regex Build( SearchOptions options, out string error )
{
error = null;
var pattern = options.UseRegex ? options.Query : Regex.Escape( options.Query );
// Like \b, but also works when the query starts or ends with a symbol
if ( options.WholeWord ) pattern = $@"(?<![\w]){pattern}(?![\w])";
var flags = RegexOptions.Multiline | RegexOptions.CultureInvariant;
if ( !options.MatchCase ) flags |= RegexOptions.IgnoreCase;
try
{
return new Regex( pattern, flags, TimeSpan.FromMilliseconds( 250 ) );
}
catch ( ArgumentException e )
{
error = e.Message;
return null;
}
}
private static TextPos ToPos( int[] lineStarts, int offset )
{
var line = Array.BinarySearch( lineStarts, offset );
if ( line < 0 ) line = ~line - 1;
return new TextPos( line, offset - lineStarts[line] );
}
}