mirror of
https://github.com/stride3d/stride.git
synced 2026-10-12 02:23:23 +00:00
Stride.VisualStudio.Package: add SDSL TextMate grammar generator (unused)
Generate sdsl.tmLanguage.json from Stride's shader vocabulary (Reserved.cs) via a .NET 10 file-based generator, so the coloring never drifts from the language. Not wired into anything yet: the out-of-process extension can't register a TextMate grammar (needs a classic pkgdef asset). Kept for a future VS Code extension / LSP baseline; see syntaxes/README.md.
This commit is contained in:
@@ -19,6 +19,12 @@
|
||||
<NoWarn>$(NoWarn);VSEXTPREVIEW_OUTPUTWINDOW</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- syntaxes/ holds the generated SDSL TextMate grammar and its 'dotnet run' generator script. The grammar
|
||||
is NOT consumed yet — it's kept for future use (a VS Code extension, or the LSP's baseline coloring) and
|
||||
is deliberately not packed here: the out-of-process model can't register a TextMate grammar (that needs
|
||||
a classic pkgdef asset, see syntaxes/README.md). The generator .cs has top-level statements, so keep it
|
||||
out of the extension compilation. -->
|
||||
<Compile Remove="syntaxes\**\*.cs" />
|
||||
<!-- Extension icon referenced by ExtensionConfiguration.Metadata.Icon (Content is IncludeInVSIX by default). -->
|
||||
<Content Include="Resources\GameStudio.png" />
|
||||
<!-- RemoteUserControl XAML: embedded (loaded by type name), not compiled as a WPF Page. -->
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# SDSL TextMate grammar (not used yet — kept for future use)
|
||||
|
||||
`sdsl.tmLanguage.json` is a TextMate grammar that colorizes Stride shader files (`.sdsl`/`.sdfx`). It is
|
||||
**generated** from Stride's own shader vocabulary by `generate-sdsl-grammar.cs`, so the keyword/type lists never
|
||||
drift from the language.
|
||||
|
||||
## Status: not wired into anything
|
||||
|
||||
This grammar is **not currently consumed** by any shipping component. It is not packed into this Visual Studio
|
||||
extension, because the out-of-process VisualStudio.Extensibility model has no way to register a TextMate grammar
|
||||
— that requires a classic (in-proc) VSIX pkgdef asset, which the generated out-of-process manifest can't carry.
|
||||
It's committed here so the work isn't lost and is ready to adopt.
|
||||
|
||||
## Intended future use
|
||||
|
||||
- **VS Code extension** — drop this file in and reference it from `package.json` (`contributes.grammars`); coloring
|
||||
is instant and trivial there.
|
||||
- **Visual Studio** — coloring will come from an **LSP** (`LanguageServerProvider`, which *is* supported
|
||||
out-of-process) via semantic tokens; this grammar can serve as the fast offline baseline if paired with a
|
||||
classic language-configuration VSIX later.
|
||||
|
||||
Before adopting it, regenerate to pick up any vocabulary changes (see below), and re-add a freshness check
|
||||
(build the parser, regenerate, `git diff`) to whatever consumes it.
|
||||
|
||||
## Regenerating
|
||||
|
||||
Requires a built `Stride.Shaders.Parsers.dll` (any recent engine build produces one). Then:
|
||||
|
||||
```
|
||||
dotnet run generate-sdsl-grammar.cs
|
||||
```
|
||||
|
||||
It auto-locates the newest `net10.0` build of the parser under `sources/shaders/Stride.Shaders.Parsers/bin`, or
|
||||
takes an explicit path: `dotnet run generate-sdsl-grammar.cs -- <path-to-Stride.Shaders.Parsers.dll>`.
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
|
||||
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
|
||||
|
||||
// Generates sdsl.tmLanguage.json, a TextMate grammar for syntax-highlighting .sdsl/.sdfx files, from Stride's
|
||||
// own shader vocabulary so the coloring never drifts from the language.
|
||||
//
|
||||
// STATUS: the generated grammar is NOT used by anything yet — it's kept for future use (a VS Code extension, or
|
||||
// the LSP's baseline coloring). It is intentionally not packed into the VS extension. See README.md in this folder.
|
||||
//
|
||||
// The keyword/type lists live in Stride.Shaders.Parsing.SDSL.Reserved (Stride.Shaders.Parsers). They're built
|
||||
// at runtime in a static constructor (loops that expand float -> float4, float4x4, ...), so this reflects the
|
||||
// *built* assembly and reads the finished sets rather than trying to parse the source. Only the vocabulary is
|
||||
// generated; the structural rules (comments/strings/numbers) are the hand-written template below.
|
||||
//
|
||||
// Run it as a .NET 10 file-based app (no project needed):
|
||||
// dotnet run sources/tools/Stride.VisualStudio.Package/syntaxes/generate-sdsl-grammar.cs
|
||||
// It finds a built Stride.Shaders.Parsers.dll under sources/shaders automatically, or takes one as an argument:
|
||||
// dotnet run .../generate-sdsl-grammar.cs -- "path/to/Stride.Shaders.Parsers.dll"
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
var scriptDir = ScriptDirectory();
|
||||
var repoRoot = FindRepoRoot(scriptDir);
|
||||
|
||||
var dllPath = args.Length > 0 ? args[0] : LocateParsersAssembly(repoRoot);
|
||||
if (dllPath is null || !File.Exists(dllPath))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"Could not find a built Stride.Shaders.Parsers.dll. Build Stride.Shaders.Parsers (or the engine) first, " +
|
||||
"or pass the assembly path: dotnet run generate-sdsl-grammar.cs -- <path-to-dll>");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Reading SDSL vocabulary from {dllPath}");
|
||||
|
||||
// Reflect the two vocabulary sets. The type is public; the fields are internal, so read them with reflection.
|
||||
var assembly = Assembly.LoadFrom(dllPath);
|
||||
var reserved = assembly.GetType("Stride.Shaders.Parsing.SDSL.Reserved", throwOnError: true)!;
|
||||
var typeNames = ReadStringSet(reserved, "TypeNames");
|
||||
var keywords = ReadStringSet(reserved, "Keywords");
|
||||
|
||||
// Types = the numbered scalar/vector/matrix names plus the bare scalars they derive from (float alongside
|
||||
// float4), so they colour consistently. Everything else reserved is a keyword.
|
||||
var types = new SortedSet<string>(typeNames, StringComparer.Ordinal);
|
||||
foreach (var name in typeNames)
|
||||
{
|
||||
var baseName = Regex.Match(name, @"^(.*?)\d+(x\d+)?$");
|
||||
if (baseName.Success && keywords.Contains(baseName.Groups[1].Value))
|
||||
types.Add(baseName.Groups[1].Value);
|
||||
}
|
||||
var keywordOnly = new SortedSet<string>(keywords.Except(types), StringComparer.Ordinal);
|
||||
|
||||
Console.WriteLine($" {types.Count} types, {keywordOnly.Count} keywords");
|
||||
|
||||
var grammar = BuildGrammar(types, keywordOnly);
|
||||
var outputPath = Path.Combine(scriptDir, "sdsl.tmLanguage.json");
|
||||
File.WriteAllText(outputPath, grammar.ToJsonString(new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
}) + Environment.NewLine);
|
||||
|
||||
Console.WriteLine($"Wrote {outputPath}");
|
||||
return 0;
|
||||
|
||||
static SortedSet<string> ReadStringSet(Type type, string fieldName)
|
||||
{
|
||||
var field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static)
|
||||
?? throw new InvalidOperationException($"{type.FullName}.{fieldName} not found (did the vocabulary move?).");
|
||||
var values = (IEnumerable<string>)field.GetValue(null)!;
|
||||
return new SortedSet<string>(values, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
// The grammar: hand-written structural rules (comments/strings/numbers) + the two generated vocabulary matches.
|
||||
static JsonObject BuildGrammar(SortedSet<string> types, SortedSet<string> keywords) => new()
|
||||
{
|
||||
["information_for_contributors"] = new JsonArray(
|
||||
"Generated by generate-sdsl-grammar.cs from Stride.Shaders.Parsing.SDSL.Reserved.",
|
||||
"Do not edit the types/keywords matches by hand — rerun the generator to update them."),
|
||||
["name"] = "Stride Shader Language",
|
||||
["scopeName"] = "source.sdsl",
|
||||
["fileTypes"] = new JsonArray("sdsl", "sdfx"),
|
||||
["patterns"] = new JsonArray(
|
||||
Include("#comments"),
|
||||
Include("#strings"),
|
||||
Include("#numbers"),
|
||||
Include("#types"),
|
||||
Include("#keywords")),
|
||||
["repository"] = new JsonObject
|
||||
{
|
||||
["comments"] = new JsonObject
|
||||
{
|
||||
["patterns"] = new JsonArray(
|
||||
new JsonObject { ["name"] = "comment.line.double-slash.sdsl", ["match"] = @"//.*$" },
|
||||
new JsonObject { ["name"] = "comment.block.sdsl", ["begin"] = @"/\*", ["end"] = @"\*/" }),
|
||||
},
|
||||
["strings"] = new JsonObject
|
||||
{
|
||||
["name"] = "string.quoted.double.sdsl",
|
||||
["begin"] = "\"",
|
||||
["end"] = "\"",
|
||||
["patterns"] = new JsonArray(new JsonObject { ["name"] = "constant.character.escape.sdsl", ["match"] = @"\\." }),
|
||||
},
|
||||
["numbers"] = new JsonObject
|
||||
{
|
||||
["name"] = "constant.numeric.sdsl",
|
||||
["match"] = @"\b(0[xX][0-9a-fA-F]+|\d+\.?\d*([eE][+-]?\d+)?)[fFhHlLuU]*\b",
|
||||
},
|
||||
["types"] = Words(types, "storage.type.sdsl"),
|
||||
["keywords"] = Words(keywords, "keyword.other.sdsl"),
|
||||
},
|
||||
};
|
||||
|
||||
static JsonObject Include(string reference) => new() { ["include"] = reference };
|
||||
|
||||
static JsonObject Words(IEnumerable<string> words, string scope) => new()
|
||||
{
|
||||
["name"] = scope,
|
||||
["match"] = $@"\b({string.Join("|", words.Select(Regex.Escape))})\b",
|
||||
};
|
||||
|
||||
static string ScriptDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!;
|
||||
|
||||
// Walk up to the repository root (the directory that holds the sources/ tree).
|
||||
static string FindRepoRoot(string start)
|
||||
{
|
||||
for (var dir = new DirectoryInfo(start); dir is not null; dir = dir.Parent)
|
||||
if (Directory.Exists(Path.Combine(dir.FullName, "sources")))
|
||||
return dir.FullName;
|
||||
throw new DirectoryNotFoundException($"Could not find the repository root above '{start}'.");
|
||||
}
|
||||
|
||||
// Newest net10.0 (non-Android) build of Stride.Shaders.Parsers.dll under sources/shaders.
|
||||
static string? LocateParsersAssembly(string repoRoot)
|
||||
{
|
||||
var searchRoot = Path.Combine(repoRoot, "sources", "shaders", "Stride.Shaders.Parsers", "bin");
|
||||
if (!Directory.Exists(searchRoot))
|
||||
return null;
|
||||
|
||||
return Directory.EnumerateFiles(searchRoot, "Stride.Shaders.Parsers.dll", SearchOption.AllDirectories)
|
||||
.Where(path => path.Contains($"{Path.DirectorySeparatorChar}net10.0{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
||||
&& !path.Contains("android", StringComparison.OrdinalIgnoreCase)
|
||||
&& !path.Contains($"{Path.DirectorySeparatorChar}ref{Path.DirectorySeparatorChar}", StringComparison.Ordinal)
|
||||
&& !path.Contains($"{Path.DirectorySeparatorChar}refint{Path.DirectorySeparatorChar}", StringComparison.Ordinal))
|
||||
.OrderByDescending(File.GetLastWriteTimeUtc)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"information_for_contributors": [
|
||||
"Generated by generate-sdsl-grammar.cs from Stride.Shaders.Parsing.SDSL.Reserved.",
|
||||
"Do not edit the types/keywords matches by hand — rerun the generator to update them."
|
||||
],
|
||||
"name": "Stride Shader Language",
|
||||
"scopeName": "source.sdsl",
|
||||
"fileTypes": [
|
||||
"sdsl",
|
||||
"sdfx"
|
||||
],
|
||||
"patterns": [
|
||||
{
|
||||
"include": "#comments"
|
||||
},
|
||||
{
|
||||
"include": "#strings"
|
||||
},
|
||||
{
|
||||
"include": "#numbers"
|
||||
},
|
||||
{
|
||||
"include": "#types"
|
||||
},
|
||||
{
|
||||
"include": "#keywords"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"comments": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "comment.line.double-slash.sdsl",
|
||||
"match": "//.*$"
|
||||
},
|
||||
{
|
||||
"name": "comment.block.sdsl",
|
||||
"begin": "/\\*",
|
||||
"end": "\\*/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"name": "string.quoted.double.sdsl",
|
||||
"begin": "\"",
|
||||
"end": "\"",
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.sdsl",
|
||||
"match": "\\\\."
|
||||
}
|
||||
]
|
||||
},
|
||||
"numbers": {
|
||||
"name": "constant.numeric.sdsl",
|
||||
"match": "\\b(0[xX][0-9a-fA-F]+|\\d+\\.?\\d*([eE][+-]?\\d+)?)[fFhHlLuU]*\\b"
|
||||
},
|
||||
"types": {
|
||||
"name": "storage.type.sdsl",
|
||||
"match": "\\b(bool|bool1|bool1x1|bool1x2|bool1x3|bool1x4|bool2|bool2x1|bool2x2|bool2x3|bool2x4|bool3|bool3x1|bool3x2|bool3x3|bool3x4|bool4|bool4x1|bool4x2|bool4x3|bool4x4|byte1|byte1x1|byte1x2|byte1x3|byte1x4|byte2|byte2x1|byte2x2|byte2x3|byte2x4|byte3|byte3x1|byte3x2|byte3x3|byte3x4|byte4|byte4x1|byte4x2|byte4x3|byte4x4|double|double1|double1x1|double1x2|double1x3|double1x4|double2|double2x1|double2x2|double2x3|double2x4|double3|double3x1|double3x2|double3x3|double3x4|double4|double4x1|double4x2|double4x3|double4x4|float|float1|float1x1|float1x2|float1x3|float1x4|float2|float2x1|float2x2|float2x3|float2x4|float3|float3x1|float3x2|float3x3|float3x4|float4|float4x1|float4x2|float4x3|float4x4|half|half1|half1x1|half1x2|half1x3|half1x4|half2|half2x1|half2x2|half2x3|half2x4|half3|half3x1|half3x2|half3x3|half3x4|half4|half4x1|half4x2|half4x3|half4x4|int|int1|int1x1|int1x2|int1x3|int1x4|int2|int2x1|int2x2|int2x3|int2x4|int3|int3x1|int3x2|int3x3|int3x4|int4|int4x1|int4x2|int4x3|int4x4|long1|long1x1|long1x2|long1x3|long1x4|long2|long2x1|long2x2|long2x3|long2x4|long3|long3x1|long3x2|long3x3|long3x4|long4|long4x1|long4x2|long4x3|long4x4|min10float|min10float1|min10float1x1|min10float1x2|min10float1x3|min10float1x4|min10float2|min10float2x1|min10float2x2|min10float2x3|min10float2x4|min10float3|min10float3x1|min10float3x2|min10float3x3|min10float3x4|min10float4|min10float4x1|min10float4x2|min10float4x3|min10float4x4|min12int|min12int1|min12int1x1|min12int1x2|min12int1x3|min12int1x4|min12int2|min12int2x1|min12int2x2|min12int2x3|min12int2x4|min12int3|min12int3x1|min12int3x2|min12int3x3|min12int3x4|min12int4|min12int4x1|min12int4x2|min12int4x3|min12int4x4|min16float|min16float1|min16float1x1|min16float1x2|min16float1x3|min16float1x4|min16float2|min16float2x1|min16float2x2|min16float2x3|min16float2x4|min16float3|min16float3x1|min16float3x2|min16float3x3|min16float3x4|min16float4|min16float4x1|min16float4x2|min16float4x3|min16float4x4|min16int|min16int1|min16int1x1|min16int1x2|min16int1x3|min16int1x4|min16int2|min16int2x1|min16int2x2|min16int2x3|min16int2x4|min16int3|min16int3x1|min16int3x2|min16int3x3|min16int3x4|min16int4|min16int4x1|min16int4x2|min16int4x3|min16int4x4|min16uint|min16uint1|min16uint1x1|min16uint1x2|min16uint1x3|min16uint1x4|min16uint2|min16uint2x1|min16uint2x2|min16uint2x3|min16uint2x4|min16uint3|min16uint3x1|min16uint3x2|min16uint3x3|min16uint3x4|min16uint4|min16uint4x1|min16uint4x2|min16uint4x3|min16uint4x4|sbyte1|sbyte1x1|sbyte1x2|sbyte1x3|sbyte1x4|sbyte2|sbyte2x1|sbyte2x2|sbyte2x3|sbyte2x4|sbyte3|sbyte3x1|sbyte3x2|sbyte3x3|sbyte3x4|sbyte4|sbyte4x1|sbyte4x2|sbyte4x3|sbyte4x4|short1|short1x1|short1x2|short1x3|short1x4|short2|short2x1|short2x2|short2x3|short2x4|short3|short3x1|short3x2|short3x3|short3x4|short4|short4x1|short4x2|short4x3|short4x4|uint|uint1|uint1x1|uint1x2|uint1x3|uint1x4|uint2|uint2x1|uint2x2|uint2x3|uint2x4|uint3|uint3x1|uint3x2|uint3x3|uint3x4|uint4|uint4x1|uint4x2|uint4x3|uint4x4|ushort1|ushort1x1|ushort1x2|ushort1x3|ushort1x4|ushort2|ushort2x1|ushort2x2|ushort2x3|ushort2x4|ushort3|ushort3x1|ushort3x2|ushort3x3|ushort3x4|ushort4|ushort4x1|ushort4x2|ushort4x3|ushort4x4)\\b"
|
||||
},
|
||||
"keywords": {
|
||||
"name": "keyword.other.sdsl",
|
||||
"match": "\\b(AppendStructuredBuffer|BlendState|Buffer|ByteAddressBuffer|CompileShader|ComputeShader|ConsumeStructuredBuffer|DepthStencilState|DepthStencilView|DomainShader|GeometryShader|Hullshader|InputPatch|LineStream|NULL|OutputPatch|PixelShader|PointStream|RWBuffer|RWByteAddressBuffer|RWStructuredBuffer|RWTexture1D|RWTexture1DArray|RWTexture2D|RWTexture2DArray|RWTexture3D|RasterizerState|RenderTargetView|SamplerComparisonState|SamplerState|StructuredBuffer|Texture1D|Texture1DArray|Texture2D|Texture2DArray|Texture2DMS|Texture2DMSArray|Texture3D|TextureCube|TextureCubeArray|TriangleStream|VertexShader|asm|asm_fragment|break|case|cbuffer|centroid|class|column_major|compile|compile_fragment|compose|const|continue|default|discard|do|dword|else|export|extern|false|for|foreach|fxgroup|groupshared|if|in|inline|inout|interface|line|lineadj|linear|matrix|namespace|nointerpolation|noperspective|out|packoffset|pass|pixelfragment|point|precise|register|return|rgroup|row_major|sample|sampler|shader|shared|snorm|stateblock|stateblock_state|static|stream|string|struct|switch|tbuffer|technique|technique10|technique11|texture|triangle|triangleadj|true|typedef|uniform|unorm|unsigned|var|vector|vertexfragment|void|volatile|while)\\b"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user