Editor/Prism/Core/SourceMap.cs

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.

File Access
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();
	}
}