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.
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;
}
}
}