Editor/Prism/Text/LanguageDb/LanguageDefinition.cs

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.

File Access
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&amp;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 &lt; annotation; … &gt;;</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&amp;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&amp;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&amp;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&amp;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&amp;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
		};
	}
}