SourceMap and related structs for mapping generated shader/source lines back to graph nodes and ports. Records per-line origins, node line ranges, supports shifting, merging, lookups for node or nearest node, and enumeration of entries and nodes.
namespace Editor.Prism.Core;
/// <summary>An inclusive 1-based line range in a generated artifact.</summary>
public readonly record struct LineRange( int Start, int End )
{
/// <summary>The empty range.</summary>
public static readonly LineRange Empty = new( 0, 0 );
/// <summary>True when the range covers at least one line.</summary>
public bool IsValid => Start > 0 && End >= Start;
/// <summary>Number of lines covered.</summary>
public int Length => IsValid ? End - Start + 1 : 0;
/// <summary>True when <paramref name="line"/> falls inside the range.</summary>
public bool Contains( int line ) => IsValid && line >= Start && line <= End;
/// <inheritdoc/>
public override string ToString() => Start == End ? $"{Start}" : $"{Start}-{End}";
}
/// <summary>One generated line and where it came from.</summary>
public readonly record struct SourceMapEntry( int Line, NodeId Node, PortId? Port );
/// <summary>
/// The bidirectional map between generated shader text and the graph.
/// <para>
/// Backends call <see cref="Add(int, NodeId)"/> as they write each line; the toolchain inverts it to
/// turn a raw compiler line number into a node selection. This is the feature the built-in editor
/// structurally cannot have — its error type carries no line information at all.
/// </para>
/// </summary>
public sealed class SourceMap
{
readonly Dictionary<int, SourceMapEntry> _byLine = new();
readonly Dictionary<NodeId, LineRange> _byNode = new();
/// <summary>Name of the artifact this map describes, e.g. <c>preview.shader</c>.</summary>
public string File { get; set; }
/// <summary>Every recorded line, ordered by line number.</summary>
public IEnumerable<SourceMapEntry> Entries => _byLine.Values.OrderBy( x => x.Line );
/// <summary>Number of mapped lines.</summary>
public int Count => _byLine.Count;
/// <summary>Record that the given generated line was produced by a node.</summary>
public void Add( int line, NodeId origin ) => Add( line, origin, null );
/// <summary>Record that the given generated line was produced by a node's port.</summary>
public void Add( int line, NodeId origin, PortId? port )
{
if ( line <= 0 || !origin.IsValid ) return;
_byLine[line] = new SourceMapEntry( line, origin, port );
if ( _byNode.TryGetValue( origin, out var range ) && range.IsValid )
{
_byNode[origin] = new LineRange( Math.Min( range.Start, line ), Math.Max( range.End, line ) );
}
else
{
_byNode[origin] = new LineRange( line, line );
}
}
/// <summary>Which node produced a generated line.</summary>
public bool TryGetNode( int line, out NodeId node )
{
if ( _byLine.TryGetValue( line, out var entry ) )
{
node = entry.Node;
return node.IsValid;
}
node = NodeId.None;
return false;
}
/// <summary>The full entry for a generated line, including the port when one was recorded.</summary>
public bool TryGetEntry( int line, out SourceMapEntry entry ) => _byLine.TryGetValue( line, out entry );
/// <summary>The span of generated lines a node produced.</summary>
public bool TryGetLines( NodeId node, out LineRange range ) => _byNode.TryGetValue( node, out range );
/// <summary>
/// The nearest mapped line at or above <paramref name="line"/>. Compiler errors often land on a
/// blank or brace line; walking upward finds the statement that owns it.
/// </summary>
public bool TryGetNearestNode( int line, out NodeId node, int maxDistance = 8 )
{
for ( int i = 0; i <= maxDistance; i++ )
{
if ( TryGetNode( line - i, out node ) ) return true;
}
node = NodeId.None;
return false;
}
/// <summary>Every node that contributed to this artifact.</summary>
public IEnumerable<NodeId> Nodes => _byNode.Keys;
/// <summary>Shift every recorded line by <paramref name="delta"/>. Used when a header is prepended.</summary>
public SourceMap Shift( int delta )
{
if ( delta == 0 ) return this;
var shifted = new SourceMap { File = File };
foreach ( var entry in _byLine.Values )
{
shifted.Add( entry.Line + delta, entry.Node, entry.Port );
}
return shifted;
}
/// <summary>Copy every entry of <paramref name="other"/> into this map, offset by <paramref name="lineOffset"/>.</summary>
public void Merge( SourceMap other, int lineOffset = 0 )
{
if ( other is null ) return;
foreach ( var entry in other._byLine.Values )
{
Add( entry.Line + lineOffset, entry.Node, entry.Port );
}
}
/// <summary>Forget everything.</summary>
public void Clear()
{
_byLine.Clear();
_byNode.Clear();
}
}