Editor/Prism/Text/BracketMatcher.cs

Editor-side bracket matching utility. It finds matching bracket pairs ((), [], {}) and quote auto-close behavior, skipping matches inside lexer-marked inert regions like comments and strings, and provides forward/backward scans and enclosing-pair lookup.

Reflection
using Editor.Prism.Text.Lexer;

namespace Editor.Prism.Text;

/// <summary>A resolved bracket pair.</summary>
public readonly record struct BracketMatch( TextPosition Open, TextPosition Close, char OpenChar, char CloseChar, bool Found )
{
	/// <summary>A "no match" result.</summary>
	public static readonly BracketMatch None = default;

	/// <summary>The range spanned by the pair, inclusive of both brackets.</summary>
	public TextRange Range => new( Open, new TextPosition( Close.Line, Close.Column + 1 ) );
}

/// <summary>
/// Finds matching brackets, ignoring anything the lexer says is a comment or a string. Used for the
/// matching-bracket highlight, smart <c>Enter</c>, auto-close skip-over and the folding model.
/// </summary>
public sealed class BracketMatcher
{
	const string OpenChars = "([{";
	const string CloseChars = ")]}";

	readonly TextDocument _document;

	/// <summary>Creates a matcher over a document, optionally consulting a lexer for inert regions.</summary>
	public BracketMatcher( TextDocument document, IncrementalLexer lexer = null )
	{
		_document = document;
		Lexer = lexer;
	}

	/// <summary>Optional lexer used to skip brackets inside comments and strings.</summary>
	public IncrementalLexer Lexer { get; set; }

	/// <summary>How far the matcher will search in either direction before giving up.</summary>
	public int MaxScanLines { get; set; } = 4000;

	/// <summary>Whether a character opens a bracket pair.</summary>
	public static bool IsOpen( char c ) => OpenChars.IndexOf( c ) >= 0;

	/// <summary>Whether a character closes a bracket pair.</summary>
	public static bool IsClose( char c ) => CloseChars.IndexOf( c ) >= 0;

	/// <summary>The closing bracket for an opening one, or <c>'\0'</c>.</summary>
	public static char CloseFor( char open )
	{
		var index = OpenChars.IndexOf( open );
		return index < 0 ? '\0' : CloseChars[index];
	}

	/// <summary>The opening bracket for a closing one, or <c>'\0'</c>.</summary>
	public static char OpenFor( char close )
	{
		var index = CloseChars.IndexOf( close );
		return index < 0 ? '\0' : OpenChars[index];
	}

	/// <summary>Whether a character participates in auto-closing, including quotes.</summary>
	public static bool IsAutoClosePair( char c ) => IsOpen( c ) || c == '"' || c == '\'';

	/// <summary>The auto-close partner for a character, including quotes.</summary>
	public static char AutoCloseFor( char c )
	{
		if ( c == '"' ) return '"';
		if ( c == '\'' ) return '\'';
		return CloseFor( c );
	}

	/// <summary>
	/// Matches the bracket at the caret. Looks at the character to the right first, then the character
	/// to the left, which is what every editor does and what users expect.
	/// </summary>
	public bool TryMatchAt( TextPosition caret, out BracketMatch match )
	{
		match = BracketMatch.None;

		if ( _document is null )
			return false;

		var position = _document.Clamp( caret );
		var line = _document.GetLine( position.Line );

		if ( position.Column < line.Length && TryMatchBracketAt( new TextPosition( position.Line, position.Column ), out match ) )
			return true;

		if ( position.Column > 0 && TryMatchBracketAt( new TextPosition( position.Line, position.Column - 1 ), out match ) )
			return true;

		match = BracketMatch.None;
		return false;
	}

	/// <summary>Matches the bracket at an exact character position.</summary>
	public bool TryMatchBracketAt( TextPosition position, out BracketMatch match )
	{
		match = BracketMatch.None;

		if ( _document is null )
			return false;

		var line = _document.GetLine( position.Line );

		if ( position.Column < 0 || position.Column >= line.Length )
			return false;

		var c = line[position.Column];

		if ( !IsOpen( c ) && !IsClose( c ) )
			return false;

		if ( IsInert( position ) )
			return false;

		if ( IsOpen( c ) )
		{
			if ( !ScanForward( position, c, CloseFor( c ), out var close ) )
				return false;

			match = new BracketMatch( position, close, c, CloseFor( c ), true );
			return true;
		}

		if ( !ScanBackward( position, OpenFor( c ), c, out var open ) )
			return false;

		match = new BracketMatch( open, position, OpenFor( c ), c, true );
		return true;
	}

	/// <summary>
	/// Finds the innermost pair enclosing a position. Used by smart indent and by "select enclosing
	/// block".
	/// </summary>
	public bool TryFindEnclosing( TextPosition position, out BracketMatch match, char openChar = '{' )
	{
		match = BracketMatch.None;

		if ( _document is null )
			return false;

		var closeChar = CloseFor( openChar );

		if ( closeChar == '\0' )
			return false;

		if ( !ScanBackwardUnmatched( position, openChar, closeChar, out var open ) )
			return false;

		if ( !ScanForward( open, openChar, closeChar, out var close ) )
			return false;

		match = new BracketMatch( open, close, openChar, closeChar, true );
		return true;
	}

	/// <summary>Whether a position sits in code rather than in a comment or a string.</summary>
	public bool IsCode( TextPosition position ) => !IsInert( position );

	bool IsInert( TextPosition position )
	{
		var lexer = Lexer;

		if ( lexer is null )
			return false;

		// Cached only: a match may scan thousands of lines and must never force a whole-document lex.
		if ( !lexer.TryGetCachedToken( position, out var token ) )
			return false;

		switch ( token.Kind )
		{
			case TokenKind.Comment:
			case TokenKind.DocComment:
			case TokenKind.String:
			case TokenKind.IncludePath:
				return true;
			default:
				return false;
		}
	}

	bool ScanForward( TextPosition from, char open, char close, out TextPosition result )
	{
		result = default;

		var depth = 0;
		var lastLine = Math.Min( _document.LineCount - 1, from.Line + MaxScanLines );

		for ( var lineIndex = from.Line; lineIndex <= lastLine; lineIndex++ )
		{
			var text = _document.GetLine( lineIndex );
			var startColumn = lineIndex == from.Line ? from.Column : 0;

			for ( var column = startColumn; column < text.Length; column++ )
			{
				var c = text[column];

				if ( c != open && c != close )
					continue;

				var position = new TextPosition( lineIndex, column );

				if ( IsInert( position ) )
					continue;

				if ( c == open )
				{
					depth++;
					continue;
				}

				depth--;

				if ( depth <= 0 )
				{
					result = position;
					return true;
				}
			}
		}

		return false;
	}

	bool ScanBackward( TextPosition from, char open, char close, out TextPosition result )
	{
		result = default;

		var depth = 0;
		var firstLine = Math.Max( 0, from.Line - MaxScanLines );

		for ( var lineIndex = from.Line; lineIndex >= firstLine; lineIndex-- )
		{
			var text = _document.GetLine( lineIndex );
			var startColumn = lineIndex == from.Line ? Math.Min( from.Column, text.Length - 1 ) : text.Length - 1;

			for ( var column = startColumn; column >= 0; column-- )
			{
				var c = text[column];

				if ( c != open && c != close )
					continue;

				var position = new TextPosition( lineIndex, column );

				if ( IsInert( position ) )
					continue;

				if ( c == close )
				{
					depth++;
					continue;
				}

				depth--;

				if ( depth <= 0 )
				{
					result = position;
					return true;
				}
			}
		}

		return false;
	}

	bool ScanBackwardUnmatched( TextPosition from, char open, char close, out TextPosition result )
	{
		result = default;

		var depth = 0;
		var firstLine = Math.Max( 0, from.Line - MaxScanLines );

		for ( var lineIndex = from.Line; lineIndex >= firstLine; lineIndex-- )
		{
			var text = _document.GetLine( lineIndex );
			var startColumn = lineIndex == from.Line
				? Math.Min( from.Column - 1, text.Length - 1 )
				: text.Length - 1;

			for ( var column = startColumn; column >= 0; column-- )
			{
				var c = text[column];

				if ( c != open && c != close )
					continue;

				var position = new TextPosition( lineIndex, column );

				if ( IsInert( position ) )
					continue;

				if ( c == close )
				{
					depth++;
					continue;
				}

				if ( depth == 0 )
				{
					result = position;
					return true;
				}

				depth--;
			}
		}

		return false;
	}
}