Skip to main content
SourceText provides an efficient representation of source code with built-in line/column tracking, encoding information, and change tracking.

Static Factory Methods

From String

Creates source text from a string.
var text = SourceText.From(
    "local x = 1\nprint(x)",
    Encoding.UTF8,
    SourceHashAlgorithm.Sha256
);
text
string
required
The source code text
encoding
Encoding
default:"null"
The text encoding. If null, the source text won’t be debuggable.
checksumAlgorithm
SourceHashAlgorithm
default:"Sha1"
Hash algorithm for PDB checksum. Values: Sha1, Sha256
return
SourceText
A new SourceText instance

From Stream

Creates source text from a stream, auto-detecting encoding from BOM.
using var stream = File.OpenRead("script.lua");
var text = SourceText.From(
    stream,
    encoding: Encoding.UTF8,  // Fallback if no BOM
    checksumAlgorithm: SourceHashAlgorithm.Sha1,
    throwIfBinaryDetected: true
);
stream
Stream
required
The stream containing source text. Must be readable and seekable.
encoding
Encoding
default:"UTF8"
Encoding to use if no BOM is detected
checksumAlgorithm
SourceHashAlgorithm
default:"Sha1"
Hash algorithm for PDB checksum
throwIfBinaryDetected
bool
default:"false"
If true, throws InvalidDataException when two consecutive NUL characters are detected
return
SourceText
A new SourceText instance
The stream is left open after reading. Encoding is detected from BOM (Byte Order Mark) if present.

From Byte Array

Creates source text from a byte array.
byte[] bytes = File.ReadAllBytes("script.lua");
var text = SourceText.From(
    bytes,
    bytes.Length,
    Encoding.UTF8
);
buffer
byte[]
required
The byte array containing encoded text
length
int
required
Number of bytes to read from the buffer
encoding
Encoding
default:"UTF8"
Encoding to use if no BOM is detected
checksumAlgorithm
SourceHashAlgorithm
default:"Sha1"
Hash algorithm for PDB checksum
throwIfBinaryDetected
bool
default:"false"
If true, throws when binary content is detected

Properties

Length

public int Length { get; }
The total number of characters in the text.
var text = SourceText.From("hello");
Console.WriteLine(text.Length); // 5

Lines

public TextLineCollection Lines { get; }
Collection of all lines in the text. Lines are indexed starting from 0.
var text = SourceText.From("line1\nline2\nline3");
Console.WriteLine(text.Lines.Count); // 3
Console.WriteLine(text.Lines[1].ToString()); // "line2"

Encoding

public Encoding? Encoding { get; }
The text encoding, or null if unspecified.

ChecksumAlgorithm

public SourceHashAlgorithm ChecksumAlgorithm { get; }
The hash algorithm used for PDB checksums.

Indexer

public char this[int position] { get; }
Gets the character at the specified position.
var text = SourceText.From("hello");
Console.WriteLine(text[0]); // 'h'
Console.WriteLine(text[4]); // 'o'

Methods

GetSubText

Extracts a portion of the text.
var text = SourceText.From("local x = 1");
var subText = text.GetSubText(new TextSpan(6, 1)); // "x"
var fromPosition = text.GetSubText(6); // "x = 1"
span
TextSpan
The span of text to extract
start
int
(Overload) Starting position to the end of the text
return
SourceText
A new SourceText containing the specified portion

CopyTo

Copies a range of characters to a destination array.
var text = SourceText.From("hello world");
var chars = new char[5];
text.CopyTo(0, chars, 0, 5);
Console.WriteLine(new string(chars)); // "hello"
sourceIndex
int
required
Starting position in source text
destination
char[]
required
Destination character array
destinationIndex
int
required
Starting position in destination array
count
int
required
Number of characters to copy

GetChecksum

Computes the hash checksum for the text.
var text = SourceText.From("local x = 1", Encoding.UTF8);
ImmutableArray<byte> checksum = text.GetChecksum();
return
ImmutableArray<byte>
The computed checksum bytes

WithChanges

Creates a new SourceText with specified changes applied.
var original = SourceText.From("local x = 1");
var changes = new[]
{
    new TextChange(new TextSpan(6, 1), "y"), // Replace "x" with "y"
    new TextChange(new TextSpan(10, 1), "2")  // Replace "1" with "2"
};
var modified = original.WithChanges(changes);
Console.WriteLine(modified.ToString()); // "local y = 2"
changes
IEnumerable<TextChange>
required
The changes to apply. Changes must not overlap and should ideally be sorted.
return
SourceText
A new SourceText with changes applied

Replace

Convenience method to replace a single text span.
var text = SourceText.From("local x = 1");
var modified = text.Replace(new TextSpan(6, 1), "result");
// or
var modified2 = text.Replace(6, 1, "result");
Console.WriteLine(modified.ToString()); // "local result = 1"

GetChangeRanges

Gets the changes between this text and an older version.
var oldText = SourceText.From("hello");
var newText = SourceText.From("hello world");
var ranges = newText.GetChangeRanges(oldText);
foreach (var range in ranges)
{
    Console.WriteLine($"Changed {range.Span} to length {range.NewLength}");
}
oldText
SourceText
required
The older version to compare against
return
IReadOnlyList<TextChangeRange>
The change ranges describing the differences

GetTextChanges

Gets the actual text changes between versions.
var oldText = SourceText.From("local x = 1");
var newText = SourceText.From("local y = 2");
var textChanges = newText.GetTextChanges(oldText);
foreach (var change in textChanges)
{
    Console.WriteLine($"At {change.Span}: '{change.NewText}'");
}

Supporting Types

TextLine

Represents a single line of text.
var text = SourceText.From("line1\nline2");
TextLine line = text.Lines[0];

Console.WriteLine(line.Span);        // TextSpan of the line
Console.WriteLine(line.Start);       // Starting position
Console.WriteLine(line.End);         // Ending position (exclusive)
Console.WriteLine(line.LineNumber);  // 0-based line number
Console.WriteLine(line.ToString());  // "line1"

TextLineCollection

Collection of lines with efficient lookups.
var text = SourceText.From("a\nb\nc");
TextLineCollection lines = text.Lines;

Console.WriteLine(lines.Count);                    // 3
Console.WriteLine(lines[1].ToString());            // "b"
Console.WriteLine(lines.IndexOf(2));               // 1 (position 2 is on line 1)
TextLine line = lines.GetLineFromPosition(2);     // Gets line containing position 2

LinePosition

Represents a line and character position (0-based).
var position = new LinePosition(line: 5, character: 10);
Console.WriteLine($"Line {position.Line}, Column {position.Character}");

Usage Examples

Read File and Track Lines

var text = SourceText.From(
    File.ReadAllText("script.lua"),
    Encoding.UTF8
);

Console.WriteLine($"Total lines: {text.Lines.Count}");
Console.WriteLine($"Total characters: {text.Length}");

for (int i = 0; i < text.Lines.Count; i++)
{
    var line = text.Lines[i];
    Console.WriteLine($"Line {i + 1}: {line}");
}

Find Position from Line/Column

var text = SourceText.From("line1\nline2\nline3");

// Get character at line 1, column 2 (0-based)
int lineIndex = 1;
int columnIndex = 2;

var line = text.Lines[lineIndex];
int position = line.Start + columnIndex;
char character = text[position];

Console.WriteLine($"Character: '{character}'"); // 'n' from "line2"

Apply Incremental Edits

var text = SourceText.From("local x = 1\nlocal y = 2");

// User types at position 11 (after "1")
var edit1 = text.Replace(11, 0, "0"); // "local x = 10"

// User deletes at position 6-7 (the "x")
var edit2 = edit1.Replace(6, 1, ""); // "local  = 10"

// Get all changes from original to final
var changes = edit2.GetTextChanges(text);
foreach (var change in changes)
{
    Console.WriteLine($"Changed [{change.Span.Start}..{change.Span.End}] to '{change.NewText}'");
}

Efficient Substring Operations

var text = SourceText.From("-- Comment\nlocal x = 1");

// Extract just the code (skip comment line)
var codeLine = text.Lines[1];
var codeOnly = text.GetSubText(codeLine.Span);

Console.WriteLine(codeOnly.ToString()); // "local x = 1"

Encoding Detection

using var stream = File.OpenRead("script.lua");
var text = SourceText.From(stream);

if (text.Encoding != null)
{
    Console.WriteLine($"Detected encoding: {text.Encoding.EncodingName}");
}
else
{
    Console.WriteLine("No encoding specified");
}

Performance Notes

Line Tracking: Line information is computed lazily on first access and cached. Accessing Lines repeatedly is efficient.Large Files: Files larger than 40KB are handled with a specialized implementation to avoid Large Object Heap allocations.Immutability: SourceText is immutable. All mutation operations (WithChanges, Replace) return new instances.

See Also

Build docs developers (and LLMs) love