Defines language-specific lexical rules and metadata for the editor's shader languages. It includes CommentRules record for comment/string delimiters and LanguageDefinition class that stores keywords, types, operators, completion triggers, include paths, and lookup helpers (IsKeyword, IsIntrinsic, IsSemantic, CompletionWords, etc.). It also provides built-in static instances for HLSL, Slang and a lazily built VFX definition with a Flush method and a For(...) resolver.
namespace Editor.Prism.Text.LanguageDb;
/// <summary>
/// How one language spells its comments and strings. The lexers read this instead of hard-coding
/// <c>//</c> and <c>/* */</c>, so a future language only has to supply a different instance.
/// </summary>
/// <param name="Line">The line-comment introducer, for example <c>//</c>.</param>
/// <param name="DocLine">The documentation-comment introducer, for example <c>///</c>.</param>
/// <param name="BlockStart">The block-comment opener, for example <c>/*</c>.</param>
/// <param name="BlockEnd">The block-comment closer, for example <c>*/</c>.</param>
/// <param name="StringQuotes">Every character that opens and closes a string or character literal.</param>
/// <param name="EscapeChar">The escape character inside a string literal.</param>
public sealed record CommentRules(
string Line,
string DocLine,
string BlockStart,
string BlockEnd,
string StringQuotes,
char EscapeChar )
{
/// <summary>The C-family defaults shared by HLSL, Slang and VFX.</summary>
public static readonly CommentRules CFamily = new( "//", "///", "/*", "*/", "\"'", '\\' );
}
/// <summary>
/// Everything the text editor knows about one shading language: its word classes, its operator set,
/// its comment and string rules, the intrinsic catalogue, the s&box symbol table, the characters
/// that should pop up completion, and the include search roots. One immutable instance per language,
/// built once and shared by every open document.
/// </summary>
public sealed class LanguageDefinition
{
private static LanguageDefinition s_vfx;
/// <summary>Stable language id: <c>"hlsl"</c>, <c>"slang"</c> or <c>"vfx"</c>.</summary>
public string Id { get; init; } = "hlsl";
/// <summary>Human-readable name for the status bar and the language picker.</summary>
public string DisplayName { get; init; } = "HLSL";
/// <summary>File extensions this language claims, without the leading dot.</summary>
public IReadOnlyList<string> FileExtensions { get; init; } = Array.Empty<string>();
/// <summary>Declaration keywords: <c>struct</c>, <c>cbuffer</c>, <c>typedef</c>, …</summary>
public IReadOnlySet<string> Keywords { get; init; } = Empty;
/// <summary>Control-flow keywords: <c>if</c>, <c>for</c>, <c>discard</c>, …</summary>
public IReadOnlySet<string> ControlKeywords { get; init; } = Empty;
/// <summary>Storage, interpolation and parameter modifiers: <c>static</c>, <c>nointerpolation</c>, <c>inout</c>, …</summary>
public IReadOnlySet<string> Modifiers { get; init; } = Empty;
/// <summary>Built-in scalar, vector and matrix types, fully expanded (<c>float</c> … <c>float4x4</c>).</summary>
public IReadOnlySet<string> BuiltinTypes { get; init; } = Empty;
/// <summary>Built-in object types: textures, buffers, samplers, patches, streams.</summary>
public IReadOnlySet<string> ObjectTypes { get; init; } = Empty;
/// <summary>Literal keywords: <c>true</c>, <c>false</c>, <c>NULL</c>, …</summary>
public IReadOnlySet<string> Literals { get; init; } = Empty;
/// <summary>Attribute names legal inside <c>[ ]</c>.</summary>
public IReadOnlySet<string> Attributes { get; init; } = Empty;
/// <summary>Semantic names, without their optional trailing index.</summary>
public IReadOnlySet<string> Semantics { get; init; } = Empty;
/// <summary>Preprocessor directive names, without the leading <c>#</c>.</summary>
public IReadOnlySet<string> PreprocessorDirectives { get; init; } = Empty;
/// <summary>Macros the compiler predefines, such as <c>__FILE__</c> and <c>PROGRAM</c>.</summary>
public IReadOnlySet<string> PredefinedMacros { get; init; } = Empty;
/// <summary>Intrinsics this language adds on top of <see cref="IntrinsicDb"/>.</summary>
public IReadOnlySet<string> ExtraIntrinsics { get; init; } = Empty;
/// <summary>Multi-character operators, longest first so a greedy match is correct.</summary>
public IReadOnlyList<string> Operators { get; init; } = Array.Empty<string>();
/// <summary>Single characters treated as punctuation rather than as operators.</summary>
public string Punctuation { get; init; } = "()[]{},;";
/// <summary>Comment and string rules.</summary>
public CommentRules Comments { get; init; } = CommentRules.CFamily;
/// <summary>Characters that should open the completion popup as soon as they are typed.</summary>
public IReadOnlyList<char> CompletionTriggers { get; init; } = new[] { '.', '#', '[', ':', '<' };
/// <summary>
/// Include search roots, in the order the engine's preprocessor tries them. Relative to a
/// content root; <c>IncludeResolver</c> turns them into absolute paths.
/// </summary>
public IReadOnlyList<string> IncludeSearchPaths { get; init; } = Array.Empty<string>();
/// <summary>Includes that resolve inside the compiler DLLs and have no file on disk.</summary>
public IReadOnlySet<string> VirtualIncludes { get; init; } = Empty;
/// <summary>True when <c>type name < annotation; … >;</c> metadata is part of the language.</summary>
public bool SupportsAnnotations { get; init; }
/// <summary>True when <c>module</c> / <c>import</c> / <c>__include</c> are recognised.</summary>
public bool SupportsModules { get; init; }
/// <summary>False for s&box, whose preprocessor only matches the double-quoted include form.</summary>
public bool SupportsAngleBracketIncludes { get; init; }
/// <summary>True when the file is a VFX block file with <c>HEADER</c> / <c>MODES</c> / <c>PS</c> sections.</summary>
public bool HasVfxBlocks { get; init; }
/// <summary>The s&box symbol table is in scope in every language we edit.</summary>
public bool HasSboxSymbols { get; init; } = true;
private static readonly HashSet<string> Empty = new( StringComparer.Ordinal );
/// <summary>True when the word is a declaration keyword.</summary>
public bool IsKeyword( string word ) => word is not null && Keywords.Contains( word );
/// <summary>True when the word is a control-flow keyword.</summary>
public bool IsControlKeyword( string word ) => word is not null && ControlKeywords.Contains( word );
/// <summary>True when the word is a storage, interpolation or parameter modifier.</summary>
public bool IsModifier( string word ) => word is not null && Modifiers.Contains( word );
/// <summary>True when the word names a built-in scalar, vector, matrix or object type.</summary>
public bool IsType( string word ) =>
word is not null && ( BuiltinTypes.Contains( word ) || ObjectTypes.Contains( word ) );
/// <summary>True when the word names a built-in object type such as <c>Texture2D</c>.</summary>
public bool IsObjectType( string word ) => word is not null && ObjectTypes.Contains( word );
/// <summary>True when the word is a literal keyword.</summary>
public bool IsLiteral( string word ) => word is not null && Literals.Contains( word );
/// <summary>True when the word is a legal attribute name inside <c>[ ]</c>.</summary>
public bool IsAttribute( string word ) => word is not null && Attributes.Contains( word );
/// <summary>True when the word is a preprocessor directive name, without its <c>#</c>.</summary>
public bool IsDirective( string word ) => word is not null && PreprocessorDirectives.Contains( word );
/// <summary>True when the word is a predefined macro.</summary>
public bool IsPredefinedMacro( string word ) => word is not null && PredefinedMacros.Contains( word );
/// <summary>
/// True when the word is a semantic. Any trailing index is ignored, so both <c>TEXCOORD</c> and
/// <c>TEXCOORD7</c> match, and anything beginning <c>SV_</c> is accepted so future system values
/// never surface as an unknown identifier.
/// </summary>
public bool IsSemantic( string word )
{
if ( string.IsNullOrEmpty( word ) )
return false;
if ( Semantics.Contains( word ) )
return true;
var end = word.Length;
while ( end > 0 && word[end - 1] >= '0' && word[end - 1] <= '9' )
end--;
if ( end != word.Length && end > 0 && Semantics.Contains( word.Substring( 0, end ) ) )
return true;
return word.StartsWith( "SV_", StringComparison.Ordinal );
}
/// <summary>True when the word names an intrinsic in this language, deprecated ones included.</summary>
public bool IsIntrinsic( string word )
{
if ( string.IsNullOrEmpty( word ) )
return false;
return IntrinsicDb.Contains( word ) || ExtraIntrinsics.Contains( word );
}
/// <summary>Looks up documentation for an intrinsic. Slang-only intrinsics have no entry and return false.</summary>
public bool TryGetIntrinsic( string word, out IntrinsicDoc doc ) => IntrinsicDb.TryGet( word, out doc );
/// <summary>
/// True when the word is anything the editor recognises — keyword, type, intrinsic, attribute,
/// semantic, predefined macro or s&box symbol. Used to decide whether to warn about an
/// unknown identifier.
/// </summary>
public bool IsKnownIdentifier( string word )
{
if ( string.IsNullOrEmpty( word ) )
return false;
if ( Keywords.Contains( word ) || ControlKeywords.Contains( word ) || Modifiers.Contains( word ) )
return true;
if ( BuiltinTypes.Contains( word ) || ObjectTypes.Contains( word ) || Literals.Contains( word ) )
return true;
if ( Attributes.Contains( word ) || PredefinedMacros.Contains( word ) )
return true;
if ( IsIntrinsic( word ) )
return true;
if ( IsSemantic( word ) )
return true;
if ( HasSboxSymbols && SboxSymbols.Contains( word ) )
return true;
return HlslLanguage.IsMemberMethod( word );
}
/// <summary>
/// Every word worth offering as a plain (non-member, non-attribute) completion, in a stable
/// order: keywords, modifiers, types, literals, intrinsics, then s&box symbols.
/// </summary>
public IEnumerable<string> CompletionWords()
{
foreach ( var w in ControlKeywords ) yield return w;
foreach ( var w in Keywords ) yield return w;
foreach ( var w in Modifiers ) yield return w;
foreach ( var w in Literals ) yield return w;
foreach ( var w in BuiltinTypes ) yield return w;
foreach ( var w in ObjectTypes ) yield return w;
foreach ( var doc in IntrinsicDb.AvailableOnTarget() )
yield return doc.Name;
foreach ( var w in ExtraIntrinsics ) yield return w;
if ( !HasSboxSymbols )
yield break;
foreach ( var symbol in SboxSymbols.All )
yield return symbol.Name;
}
/// <inheritdoc/>
public override string ToString() => DisplayName;
/// <summary>The HLSL definition.</summary>
public static LanguageDefinition Hlsl => HlslLanguage.Definition;
/// <summary>The Slang definition.</summary>
public static LanguageDefinition Slang => SlangLanguage.Definition;
/// <summary>
/// The VFX (<c>.shader</c>) definition: HLSL plus the block keywords, the annotation grammar and
/// the combo declarations that only exist inside a block file.
/// </summary>
public static LanguageDefinition Vfx
{
get
{
// Read once into a local: a hotload can flush this from the main thread while the diagnostic
// worker is classifying a buffer. Building it twice by racing is harmless — it is immutable.
var definition = s_vfx;
return definition ?? ( s_vfx = BuildVfx() );
}
}
/// <summary>
/// Drops the lazily built VFX definition, which composes the HLSL word tables and the s&box
/// symbol table and would otherwise keep both alive across a hotload. <see cref="Hlsl"/> and
/// <see cref="Slang"/> are static readonly fields of their own classes, so they go with the assembly.
/// </summary>
public static void Flush() => s_vfx = null;
/// <summary>
/// Resolves a file extension, a file name, a path or a language id to its definition. Anything
/// unrecognised falls back to HLSL, which is the safe superset for shader-ish text.
/// </summary>
public static LanguageDefinition For( string fileExtensionOrLanguage )
{
if ( string.IsNullOrWhiteSpace( fileExtensionOrLanguage ) )
return Hlsl;
var key = fileExtensionOrLanguage.Trim();
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 );
switch ( key.ToLowerInvariant() )
{
case "slang":
case "slangh":
case "slang-module":
return Slang;
case "vfx":
case "shader":
case "shader_c":
return Vfx;
default:
return Hlsl;
}
}
private static LanguageDefinition BuildVfx()
{
var hlsl = HlslLanguage.Definition;
var keywords = new HashSet<string>( hlsl.Keywords, StringComparer.Ordinal );
foreach ( var name in SboxSymbols.BlockNames )
keywords.Add( name );
var attributes = new HashSet<string>( hlsl.Attributes, StringComparer.Ordinal );
return new LanguageDefinition
{
Id = "vfx",
DisplayName = "s&box Shader (VFX)",
FileExtensions = new[] { "shader" },
Keywords = keywords,
ControlKeywords = hlsl.ControlKeywords,
Modifiers = hlsl.Modifiers,
BuiltinTypes = hlsl.BuiltinTypes,
ObjectTypes = hlsl.ObjectTypes,
Literals = hlsl.Literals,
Attributes = attributes,
Semantics = hlsl.Semantics,
PreprocessorDirectives = hlsl.PreprocessorDirectives,
PredefinedMacros = hlsl.PredefinedMacros,
ExtraIntrinsics = hlsl.ExtraIntrinsics,
Operators = hlsl.Operators,
Punctuation = hlsl.Punctuation,
Comments = CommentRules.CFamily,
CompletionTriggers = new[] { '.', '#', '[', ':', '<', '(' },
IncludeSearchPaths = SboxSymbols.IncludeSearchPaths,
VirtualIncludes = SboxSymbols.VirtualIncludes,
SupportsAnnotations = true,
SupportsModules = false,
SupportsAngleBracketIncludes = false,
HasVfxBlocks = true,
HasSboxSymbols = true
};
}
}