Editor/HotCodeEditor/CodeView/TextBuffer.cs
using System;
using System.Collections.Generic;
using System.Text;
/// <summary>
/// A position in a <see cref="TextBuffer"/>: zero-based line and character index within that line.
/// </summary>
public readonly record struct TextPos( int Line, int Col ) : IComparable<TextPos>
{
public int CompareTo( TextPos other ) => Line != other.Line ? Line.CompareTo( other.Line ) : Col.CompareTo( other.Col );
public static bool operator <( TextPos a, TextPos b ) => a.CompareTo( b ) < 0;
public static bool operator >( TextPos a, TextPos b ) => a.CompareTo( b ) > 0;
public static bool operator <=( TextPos a, TextPos b ) => a.CompareTo( b ) <= 0;
public static bool operator >=( TextPos a, TextPos b ) => a.CompareTo( b ) >= 0;
public static TextPos Min( TextPos a, TextPos b ) => a <= b ? a : b;
public static TextPos Max( TextPos a, TextPos b ) => a >= b ? a : b;
}
/// <summary>
/// Plain text stored as a list of lines. Knows nothing about carets, undo or drawing.
/// </summary>
public class TextBuffer
{
public List<string> Lines { get; } = new() { "" };
/// <summary>
/// Line ending used when joining lines back into text. Detected from the loaded text.
/// </summary>
public string LineEnding { get; set; } = "\n";
public int LineCount => Lines.Count;
public string Text
{
get => string.Join( LineEnding, Lines );
set
{
value ??= "";
LineEnding = value.Contains( "\r\n" ) ? "\r\n" : "\n";
Lines.Clear();
Lines.AddRange( Normalize( value ).Split( '\n' ) );
}
}
/// <summary>
/// Converts any line endings to \n.
/// </summary>
public static string Normalize( string text ) => text.Replace( "\r\n", "\n" ).Replace( '\r', '\n' );
public TextPos Clamp( TextPos p )
{
var line = Math.Clamp( p.Line, 0, Lines.Count - 1 );
return new TextPos( line, Math.Clamp( p.Col, 0, Lines[line].Length ) );
}
public TextPos End => new( Lines.Count - 1, Lines[^1].Length );
/// <summary>
/// Where text inserted at <paramref name="start"/> would end.
/// </summary>
public static TextPos EndOf( TextPos start, string text )
{
var lastNewline = text.LastIndexOf( '\n' );
if ( lastNewline < 0 )
return new TextPos( start.Line, start.Col + text.Length );
var newlines = 0;
foreach ( var c in text ) if ( c == '\n' ) newlines++;
return new TextPos( start.Line + newlines, text.Length - lastNewline - 1 );
}
public string GetText( TextPos a, TextPos b )
{
var start = Clamp( TextPos.Min( a, b ) );
var end = Clamp( TextPos.Max( a, b ) );
if ( start.Line == end.Line )
return Lines[start.Line].Substring( start.Col, end.Col - start.Col );
var sb = new StringBuilder();
sb.Append( Lines[start.Line], start.Col, Lines[start.Line].Length - start.Col );
for ( int i = start.Line + 1; i < end.Line; i++ )
sb.Append( '\n' ).Append( Lines[i] );
sb.Append( '\n' ).Append( Lines[end.Line], 0, end.Col );
return sb.ToString();
}
public void Delete( TextPos a, TextPos b )
{
var start = Clamp( TextPos.Min( a, b ) );
var end = Clamp( TextPos.Max( a, b ) );
if ( start == end ) return;
var head = Lines[start.Line][..start.Col];
var tail = Lines[end.Line][end.Col..];
Lines.RemoveRange( start.Line + 1, end.Line - start.Line );
Lines[start.Line] = head + tail;
}
/// <summary>
/// Inserts text (which may contain newlines) and returns the position just after it.
/// </summary>
public TextPos Insert( TextPos at, string text )
{
at = Clamp( at );
text = Normalize( text );
if ( text.Length == 0 ) return at;
var line = Lines[at.Line];
var head = line[..at.Col];
var tail = line[at.Col..];
var parts = text.Split( '\n' );
if ( parts.Length == 1 )
{
Lines[at.Line] = head + text + tail;
return new TextPos( at.Line, at.Col + text.Length );
}
Lines[at.Line] = head + parts[0];
for ( int i = 1; i < parts.Length - 1; i++ )
Lines.Insert( at.Line + i, parts[i] );
Lines.Insert( at.Line + parts.Length - 1, parts[^1] + tail );
return new TextPos( at.Line + parts.Length - 1, parts[^1].Length );
}
}