Editor/Prism/Text/Lexer/ILexer.cs

Interface and registry for language lexers used by the editor. ILexer defines a resumable, line-based lexer API with language id, Lex and IsInert methods. Lexers is a static registry that lazily constructs and caches shared Hlsl, Slang and Vfx lexer instances, can flush them, and resolves a lexer from an extension, filename or language id.

Reflection
using Editor.Prism.Text.LanguageDb;

namespace Editor.Prism.Text.Lexer;

/// <summary>
/// A resumable, line-based lexer. Every scrap of cross-line state travels in <see cref="LexState"/>,
/// so a document can be re-lexed from any line and an instance can be shared by any number of
/// documents.
/// </summary>
public interface ILexer
{
	/// <summary>Language id: "hlsl", "slang" or "vfx".</summary>
	string Language { get; }

	/// <summary>Lex one line, appending tokens to <paramref name="output"/>. Returns the exit state.</summary>
	LexState Lex( string line, LexState entry, List<Token> output );

	/// <summary>True when this position is inside a comment or string (suppresses completion).</summary>
	bool IsInert( LexState state );
}

/// <summary>
/// The lexer registry. Every lexer is stateless — all per-line state travels in <see cref="LexState"/> —
/// so a single shared instance per language is safe to use from any number of documents and threads.
/// </summary>
public static class Lexers
{
	private static HlslLexer s_hlsl;
	private static SlangLexer s_slang;
	private static VfxLexer s_vfx;

	// Each getter reads its field once into a local before testing it. Diagnostics lex on a worker
	// thread while a hotload flushes on the main one, and `??=` would hand back null if the flush
	// landed between the assignment and the read. Building two instances by racing is harmless: a
	// lexer holds no state of its own.

	/// <summary>The shared HLSL lexer.</summary>
	public static ILexer Hlsl
	{
		get
		{
			var lexer = s_hlsl;
			return lexer ?? ( s_hlsl = new HlslLexer() );
		}
	}

	/// <summary>The shared Slang lexer.</summary>
	public static ILexer Slang
	{
		get
		{
			var lexer = s_slang;
			return lexer ?? ( s_slang = new SlangLexer() );
		}
	}

	/// <summary>The shared VFX (<c>.shader</c>) lexer.</summary>
	public static ILexer Vfx
	{
		get
		{
			var lexer = s_vfx;
			return lexer ?? ( s_vfx = new VfxLexer() );
		}
	}

	/// <summary>
	/// Drops the shared lexer instances so the next request builds them again. Each one holds a
	/// reference to its <see cref="LanguageDefinition"/>, so after a hotload the old instances would
	/// otherwise keep classifying against the outgoing assembly's word tables.
	/// </summary>
	public static void Flush()
	{
		s_hlsl = null;
		s_slang = null;
		s_vfx = null;
	}

	/// <summary>Resolves a language id, a file extension, a file name or a path to a lexer.</summary>
	public static ILexer For( string fileExtensionOrLanguage )
	{
		if ( string.IsNullOrWhiteSpace( fileExtensionOrLanguage ) )
			return Hlsl;

		var key = fileExtensionOrLanguage.Trim();

		// Accept a whole file name or path as well as a bare extension.
		var slash = key.LastIndexOfAny( new[] { '/', '\\' } );
		if ( slash >= 0 && slash + 1 < key.Length )
			key = key.Substring( slash + 1 );

		var dot = key.LastIndexOf( '.' );
		if ( dot >= 0 && dot + 1 < key.Length )
			key = key.Substring( dot + 1 );

		key = key.ToLowerInvariant();

		switch ( key )
		{
			case "slang":
			case "slangh":
			case "slang-module":
				return Slang;

			case "vfx":
			case "shader":
			case "shader_c":
			case "vfx_hlsl":
				return Vfx;

			case "hlsl":
			case "hlsli":
			case "fxc":
			case "fx":
			case "h":
			case "inc":
			case "glsl":
			case "vsh":
			case "psh":
				return Hlsl;

			default:
				return Hlsl;
		}
	}
}