mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
TextEdit control & a bunch of other stuff (#3436)
This commit is contained in:
committed by
GitHub
parent
db95c6284b
commit
e34935c9e2
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Robust.Shared.Input
|
||||
@@ -7,6 +8,7 @@ namespace Robust.Shared.Input
|
||||
/// Event data values for a bound key state change.
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
[DebuggerDisplay("{Function}, {State}, CF: {CanFocus}, H: {Handled}")]
|
||||
public class BoundKeyEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -70,6 +70,8 @@ namespace Robust.Shared.Input
|
||||
this.reflectionManager = reflectionManager;
|
||||
}
|
||||
|
||||
internal IEnumerable<BoundKeyFunction> AllKeyFunctions => KeyFunctionsList;
|
||||
|
||||
public void PopulateKeyFunctionsMap()
|
||||
{
|
||||
if (KeyFunctionsMap.Count != 0)
|
||||
|
||||
@@ -46,6 +46,8 @@ namespace Robust.Shared.Input
|
||||
// Cursor keys in LineEdit and such.
|
||||
public static readonly BoundKeyFunction TextCursorLeft = "TextCursorLeft";
|
||||
public static readonly BoundKeyFunction TextCursorRight = "TextCursorRight";
|
||||
public static readonly BoundKeyFunction TextCursorUp = "TextCursorUp";
|
||||
public static readonly BoundKeyFunction TextCursorDown = "TextCursorDown";
|
||||
public static readonly BoundKeyFunction TextCursorWordLeft = "TextCursorWordLeft";
|
||||
public static readonly BoundKeyFunction TextCursorWordRight = "TextCursorWordRight";
|
||||
public static readonly BoundKeyFunction TextCursorBegin = "TextCursorBegin";
|
||||
@@ -55,12 +57,15 @@ namespace Robust.Shared.Input
|
||||
public static readonly BoundKeyFunction TextCursorSelect = "TextCursorSelect";
|
||||
public static readonly BoundKeyFunction TextCursorSelectLeft = "TextCursorSelectLeft";
|
||||
public static readonly BoundKeyFunction TextCursorSelectRight = "TextCursorSelectRight";
|
||||
public static readonly BoundKeyFunction TextCursorSelectUp = "TextCursorSelectUp";
|
||||
public static readonly BoundKeyFunction TextCursorSelectDown = "TextCursorSelectDown";
|
||||
public static readonly BoundKeyFunction TextCursorSelectWordLeft = "TextCursorSelectWordLeft";
|
||||
public static readonly BoundKeyFunction TextCursorSelectWordRight = "TextCursorSelectWordRight";
|
||||
public static readonly BoundKeyFunction TextCursorSelectBegin = "TextCursorSelectBegin";
|
||||
public static readonly BoundKeyFunction TextCursorSelectEnd = "TextCursorSelectEnd";
|
||||
|
||||
public static readonly BoundKeyFunction TextBackspace = "TextBackspace";
|
||||
public static readonly BoundKeyFunction TextNewline = "TextNewline";
|
||||
public static readonly BoundKeyFunction TextSubmit = "TextSubmit";
|
||||
public static readonly BoundKeyFunction TextSelectAll = "TextSelectAll";
|
||||
public static readonly BoundKeyFunction TextCopy = "TextCopy";
|
||||
|
||||
@@ -100,6 +100,11 @@ namespace Robust.Shared.Utility
|
||||
_tags.Clear();
|
||||
}
|
||||
|
||||
public FormattedMessageRuneEnumerator EnumerateRunes()
|
||||
{
|
||||
return new FormattedMessageRuneEnumerator(this);
|
||||
}
|
||||
|
||||
/// <returns>The string without markup tags.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
@@ -192,4 +197,60 @@ namespace Robust.Shared.Utility
|
||||
public Tag this[int index] => _tags[index];
|
||||
}
|
||||
}
|
||||
|
||||
public struct FormattedMessageRuneEnumerator : IEnumerable<Rune>, IEnumerator<Rune>
|
||||
{
|
||||
private readonly FormattedMessage _msg;
|
||||
private List<FormattedMessage.Tag>.Enumerator _tagEnumerator;
|
||||
private StringRuneEnumerator _runeEnumerator;
|
||||
|
||||
internal FormattedMessageRuneEnumerator(FormattedMessage msg)
|
||||
{
|
||||
_msg = msg;
|
||||
_tagEnumerator = msg.Tags.GetEnumerator();
|
||||
// Rune enumerator will immediately give false on first iteration so I dont' need to special case anything.
|
||||
_runeEnumerator = "".EnumerateRunes();
|
||||
}
|
||||
|
||||
public IEnumerator<Rune> GetEnumerator() => this;
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
while (!_runeEnumerator.MoveNext())
|
||||
{
|
||||
FormattedMessage.TagText text;
|
||||
while (true)
|
||||
{
|
||||
var result = _tagEnumerator.MoveNext();
|
||||
if (!result)
|
||||
return false;
|
||||
|
||||
if (_tagEnumerator.Current is not FormattedMessage.TagText { Text.Length: > 0 } nextText)
|
||||
continue;
|
||||
|
||||
text = nextText;
|
||||
break;
|
||||
}
|
||||
|
||||
_runeEnumerator = text.Text.EnumerateRunes();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_tagEnumerator = _msg.Tags.GetEnumerator();
|
||||
_runeEnumerator = "".EnumerateRunes();
|
||||
}
|
||||
|
||||
public Rune Current => _runeEnumerator.Current;
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
117
Robust.Shared/Utility/StringEnumerateHelpers.cs
Normal file
117
Robust.Shared/Utility/StringEnumerateHelpers.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Robust.Shared.Utility;
|
||||
|
||||
internal static class StringEnumerateHelpers
|
||||
{
|
||||
internal struct SubstringRuneEnumerator : IEnumerable<Rune>, IEnumerator<Rune>
|
||||
{
|
||||
private readonly string _source;
|
||||
private int _nextChar;
|
||||
private Rune _current;
|
||||
|
||||
public SubstringRuneEnumerator(string source, int firstChar)
|
||||
{
|
||||
_source = source;
|
||||
_nextChar = firstChar;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_nextChar >= _source.Length)
|
||||
return false;
|
||||
|
||||
if (!Rune.TryGetRuneAt(_source, _nextChar, out _current))
|
||||
_current = Rune.ReplacementChar;
|
||||
|
||||
_nextChar += _current.Utf16SequenceLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public readonly Rune Current => _current;
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public SubstringRuneEnumerator GetEnumerator() => this;
|
||||
|
||||
IEnumerator<Rune> IEnumerable<Rune>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
internal struct SubstringReverseRuneEnumerator : IEnumerator<Rune>, IEnumerable<Rune>
|
||||
{
|
||||
private string _source;
|
||||
// Contains the next char to return.
|
||||
// If the next char is actually a (valid) surrogate pair, this is INSIDE the pair,
|
||||
// and MoveNext() has to skip more.
|
||||
private int _nextChar;
|
||||
private Rune _current;
|
||||
|
||||
public SubstringReverseRuneEnumerator(string source, int startChar)
|
||||
{
|
||||
_source = source;
|
||||
_nextChar = startChar - 1;
|
||||
_current = default;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_nextChar < 0)
|
||||
return false;
|
||||
|
||||
var chr = _source[_nextChar];
|
||||
if (!char.IsSurrogate(chr))
|
||||
{
|
||||
_current = new Rune(chr);
|
||||
}
|
||||
else if (char.IsLowSurrogate(chr) && _nextChar >= 1)
|
||||
{
|
||||
var prevChr = _source[_nextChar - 1];
|
||||
if (char.IsHighSurrogate(prevChr))
|
||||
_current = new Rune(prevChr, chr);
|
||||
else
|
||||
_current = Rune.ReplacementChar;
|
||||
}
|
||||
else
|
||||
{
|
||||
_current = Rune.ReplacementChar;
|
||||
}
|
||||
|
||||
_nextChar -= _current.Utf16SequenceLength;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public Rune Current => _current;
|
||||
|
||||
object IEnumerator.Current => Current;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
public SubstringReverseRuneEnumerator GetEnumerator() => this;
|
||||
|
||||
IEnumerator<Rune> IEnumerable<Rune>.GetEnumerator() => GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
561
Robust.Shared/Utility/TextRope.cs
Normal file
561
Robust.Shared/Utility/TextRope.cs
Normal file
@@ -0,0 +1,561 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Robust.Shared.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// A binary tree data structure for efficient storage of large mutable text.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <see href="https://en.wikipedia.org/wiki/Rope_(data_structure)">Read the Wikipedia article, nerd</see>
|
||||
/// Also read the original paper, it's useful too.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Like strings, ropes are immutable and all "mutating" operations return new copies.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// All indexing functions use <see langword="long"/> indices.
|
||||
/// While individual rope leaves cannot be larger than an <see langword="int"/>, ropes with many leaves may exceed that.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class Rope
|
||||
{
|
||||
internal static readonly int[] FibonacciSequence =
|
||||
{
|
||||
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946,
|
||||
17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309,
|
||||
3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141,
|
||||
267914296, 433494437, 701408733, 1134903170, 1836311903
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the total text length of the rope given.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a balanced tree, this is O(log n).
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
public static long CalcTotalLength(Node? node)
|
||||
{
|
||||
return node switch
|
||||
{
|
||||
Branch branch => branch.Weight + CalcTotalLength(branch.Right),
|
||||
Leaf leaf => leaf.Weight,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Move to struct enumerator with managed stack memory.
|
||||
/// <summary>
|
||||
/// Enumerate all leaves in the rope from left to right.
|
||||
/// </summary>
|
||||
public static IEnumerable<Leaf> CollectLeaves(Node node)
|
||||
{
|
||||
var stack = new Stack<Branch>();
|
||||
|
||||
var leaf = RunTillLeaf(stack, node);
|
||||
yield return leaf;
|
||||
|
||||
while (stack.TryPop(out var branch))
|
||||
{
|
||||
if (branch.Right == null)
|
||||
continue;
|
||||
|
||||
leaf = RunTillLeaf(stack, branch.Right);
|
||||
yield return leaf;
|
||||
}
|
||||
|
||||
static Leaf RunTillLeaf(Stack<Branch> stack, Node node)
|
||||
{
|
||||
while (node is Branch branch)
|
||||
{
|
||||
stack.Push(branch);
|
||||
|
||||
node = branch.Left;
|
||||
}
|
||||
|
||||
return (Leaf)node;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate all leaves in the rope from right to left.
|
||||
/// </summary>
|
||||
public static IEnumerable<Leaf> CollectLeavesReverse(Node node)
|
||||
{
|
||||
var stack = new Stack<Branch>();
|
||||
|
||||
var leaf = RunTillLeaf(stack, node);
|
||||
if (leaf != null)
|
||||
yield return leaf;
|
||||
|
||||
while (stack.TryPop(out var branch))
|
||||
{
|
||||
leaf = RunTillLeaf(stack, branch.Left);
|
||||
if (leaf != null)
|
||||
yield return leaf;
|
||||
}
|
||||
|
||||
static Leaf? RunTillLeaf(Stack<Branch> stack, Node? node)
|
||||
{
|
||||
while (node is Branch branch)
|
||||
{
|
||||
stack.Push(branch);
|
||||
|
||||
node = branch.Right;
|
||||
}
|
||||
|
||||
return (Leaf?)node;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Move to struct enumerator with managed stack memory.
|
||||
/// <summary>
|
||||
/// Enumerate all text runes in the rope from left to right.
|
||||
/// </summary>
|
||||
public static IEnumerable<Rune> EnumerateRunes(Node node)
|
||||
{
|
||||
foreach (var leaf in CollectLeaves(node))
|
||||
{
|
||||
foreach (var rune in leaf.Text.EnumerateRunes())
|
||||
{
|
||||
yield return rune;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate all text runes in the rope from left to right, starting at the specified position.
|
||||
/// </summary>
|
||||
public static IEnumerable<Rune> EnumerateRunes(Node node, long startPos)
|
||||
{
|
||||
var pos = 0L;
|
||||
|
||||
// Phase 1: skip over whole leaves that are before the start position.
|
||||
// TODO: Ideally we would navigate the binary tree properly instead of starting from the far left.
|
||||
|
||||
// ReSharper disable once GenericEnumeratorNotDisposed
|
||||
var leaves = CollectLeaves(node).GetEnumerator();
|
||||
while (leaves.MoveNext())
|
||||
{
|
||||
var leaf = leaves.Current;
|
||||
if (pos + leaf.Weight >= startPos)
|
||||
{
|
||||
goto startLeafFound;
|
||||
}
|
||||
|
||||
pos += leaf.Weight;
|
||||
}
|
||||
|
||||
// Didn't find a starting leaf, must mean that startPos >= text length. Oh well?
|
||||
yield break;
|
||||
|
||||
startLeafFound:
|
||||
|
||||
// Phase 2: start halfway through the current leaf.
|
||||
{
|
||||
foreach (var rune in leaves.Current.Text.EnumerateRunes())
|
||||
{
|
||||
if (pos >= startPos)
|
||||
{
|
||||
yield return rune;
|
||||
}
|
||||
|
||||
pos += rune.Utf16SequenceLength;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: just return everything from here on out.
|
||||
while (leaves.MoveNext())
|
||||
{
|
||||
var leaf = leaves.Current;
|
||||
foreach (var rune in leaf.Text.EnumerateRunes())
|
||||
{
|
||||
yield return rune;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate all the runes in the rope, from right to left.
|
||||
/// </summary>
|
||||
public static IEnumerable<Rune> EnumerateRunesReverse(Node node)
|
||||
{
|
||||
foreach (var leaf in CollectLeavesReverse(node))
|
||||
{
|
||||
var enumerator = new StringEnumerateHelpers.SubstringReverseRuneEnumerator(leaf.Text, leaf.Text.Length);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
yield return enumerator.Current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate all text runes in the rope from right to left, starting at the specified position.
|
||||
/// </summary>
|
||||
public static IEnumerable<Rune> EnumerateRunesReverse(Node node, long endPos)
|
||||
{
|
||||
var pos = CalcTotalLength(node);
|
||||
|
||||
// TODO: Actually start at the position instead of skipping like a worse linked list thanks.
|
||||
|
||||
foreach (var rune in EnumerateRunesReverse(node))
|
||||
{
|
||||
if (pos <= endPos)
|
||||
{
|
||||
yield return rune;
|
||||
}
|
||||
|
||||
pos -= rune.Utf16SequenceLength;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the given rope is sufficiently balanced to avoid bad performance.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static bool IsBalanced(Node node)
|
||||
{
|
||||
var depth = node.Depth;
|
||||
if (depth > FibonacciSequence.Length - 2)
|
||||
return false;
|
||||
|
||||
return FibonacciSequence[depth + 2] <= node.Weight;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure the rope is balanced to ensure decent performance on various operations.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the rope is already balanced, this method does nothing.
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
public static Node Rebalance(Node node)
|
||||
{
|
||||
if (IsBalanced(node))
|
||||
return node;
|
||||
|
||||
var leaves = CollectLeaves(node).ToArray();
|
||||
return Merge(leaves);
|
||||
|
||||
static Node Merge(ReadOnlySpan<Leaf> leaves)
|
||||
{
|
||||
if (leaves.Length == 1)
|
||||
return leaves[0];
|
||||
|
||||
if (leaves.Length == 2)
|
||||
return new Branch(leaves[0], leaves[1]);
|
||||
|
||||
var mid = leaves.Length / 2;
|
||||
return new Branch(Merge(leaves[..mid]), Merge(leaves[mid..]));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see langword="char" /> at the specified index in the rope.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a balanced tree, this is O(log n).
|
||||
/// </remarks>
|
||||
[Pure]
|
||||
public static char Index(Node rope, long index)
|
||||
{
|
||||
switch (rope)
|
||||
{
|
||||
case Branch branch:
|
||||
if (branch.Weight > index)
|
||||
return Index(branch.Left, index);
|
||||
|
||||
if (branch.Right == null)
|
||||
throw new IndexOutOfRangeException();
|
||||
|
||||
return Index(branch.Right, index - branch.Weight);
|
||||
|
||||
case Leaf leaf:
|
||||
return leaf.Text[(int)index];
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(rope));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new rope with text spliced in at an index.
|
||||
/// </summary>
|
||||
/// <param name="rope">The rope to splice text into.</param>
|
||||
/// <param name="index">The position the inserted text should start at.</param>
|
||||
/// <param name="value">The text to insert.</param>
|
||||
/// <returns>The new rope containing the spliced data.</returns>
|
||||
[Pure]
|
||||
public static Node Insert(Node rope, long index, string value)
|
||||
{
|
||||
var (left, right) = Split(rope, index);
|
||||
return Concat(left, Concat(new Leaf(value), right));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new rope concatenating two given ropes.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static Node Concat(Node left, Node right)
|
||||
{
|
||||
return new Branch(left, right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new rope concatenating a rope and a string.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static Node Concat(Node left, string right)
|
||||
{
|
||||
return Concat(left, new Leaf(right));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new rope concatenating a string with a rope.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static Node Concat(string left, Node right)
|
||||
{
|
||||
return Concat(new Leaf(left), right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return two new ropes split from the given rope at a specified index.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static (Node left, Node right) Split(Node rope, long index)
|
||||
{
|
||||
switch (rope)
|
||||
{
|
||||
case Branch branch:
|
||||
{
|
||||
if (branch.Weight > index)
|
||||
{
|
||||
var (left, right) = Split(branch.Left, index);
|
||||
return (
|
||||
Rebalance(left),
|
||||
Rebalance(new Branch(right, branch.Right))
|
||||
);
|
||||
}
|
||||
|
||||
if (branch.Weight < index)
|
||||
{
|
||||
var (left, right) = Split(branch.Right ?? Leaf.Empty, index - branch.Weight);
|
||||
return (
|
||||
Rebalance(new Branch(branch.Left, left)),
|
||||
Rebalance(right)
|
||||
);
|
||||
}
|
||||
|
||||
return (branch.Left, branch.Right ?? Leaf.Empty);
|
||||
}
|
||||
case Leaf leaf:
|
||||
{
|
||||
var left = new Leaf(leaf.Text[..(int)index]);
|
||||
var right = new Leaf(leaf.Text[(int)index..]);
|
||||
return (left, right);
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(rope));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new rope with a slice of text removed.
|
||||
/// </summary>
|
||||
/// <param name="rope">The rope to copy.</param>
|
||||
/// <param name="start">The position to start removing chars at.</param>
|
||||
/// <param name="length">How many chars to remove.</param>
|
||||
[Pure]
|
||||
public static Node Delete(Node rope, long start, long length)
|
||||
{
|
||||
var (left, _) = Split(rope, start);
|
||||
var (_, right) = Split(rope, start + length);
|
||||
|
||||
return Concat(left, right);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new rope with a given slice of text replaced with a new string.
|
||||
/// </summary>
|
||||
/// <param name="rope">The rope to copy.</param>
|
||||
/// <param name="start">The position to start removing characters at, and insert the new text at.</param>
|
||||
/// <param name="length">How many characters from the original rope to remove.</param>
|
||||
/// <param name="text">The new text to insert at the start position.</param>
|
||||
[Pure]
|
||||
public static Node ReplaceSubstring(Node rope, long start, long length, string text)
|
||||
{
|
||||
var (left, mid) = Split(rope, start);
|
||||
var (_, right) = Split(mid, length);
|
||||
|
||||
return Concat(left, Concat(text, right));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to fetch a <see cref="Rune"/> at a certain position in the rune.
|
||||
/// Fails if the given position is inside a surrogate pair.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static bool TryGetRuneAt(Node rope, long index, out Rune value)
|
||||
{
|
||||
var chr = Index(rope, index);
|
||||
if (!char.IsSurrogate(chr))
|
||||
{
|
||||
value = new Rune(chr);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (char.IsLowSurrogate(chr))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: throws if a high surrogate is at the very end of the rope.
|
||||
var lowChr = Index(rope, index + 1);
|
||||
if (!char.IsLowSurrogate(lowChr))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = new Rune(chr, lowChr);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collapse the rope into a single string instance.
|
||||
/// </summary>
|
||||
/// <exception cref="OverflowException">The given rope is too large to fit in a single string.</exception>
|
||||
[Pure]
|
||||
public static string Collapse(Node rope)
|
||||
{
|
||||
var length = CalcTotalLength(rope);
|
||||
|
||||
return string.Create(checked((int)length), rope, static (span, node) =>
|
||||
{
|
||||
foreach (var leaf in CollectLeaves(node))
|
||||
{
|
||||
var text = leaf.Text;
|
||||
text.CopyTo(span);
|
||||
span = span[text.Length..];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collapse a substring of a rope into a single string instance.
|
||||
/// </summary>
|
||||
/// <param name="rope">The rope to collapse part of.</param>
|
||||
/// <param name="range">The range of the substring to collapse.</param>
|
||||
/// <exception cref="OverflowException">The given rope is too large to fit in a single string.</exception>
|
||||
[Pure]
|
||||
public static string CollapseSubstring(Node rope, Range range)
|
||||
{
|
||||
// TODO: Optimize
|
||||
return Collapse(rope)[range];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offset a cursor position in a rope to the left, skipping over the middle of surrogate pairs.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static long RuneShiftLeft(long index, Node rope)
|
||||
{
|
||||
index -= 1;
|
||||
if (char.IsLowSurrogate(Index(rope, index)))
|
||||
index -= 1;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Offset a cursor position in a rope to the right, skipping over the middle of surrogate pairs.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static long RuneShiftRight(long index, Node rope)
|
||||
{
|
||||
index += 1;
|
||||
|
||||
// Before you confuse yourself on "shouldn't this be high surrogate since shifting left checks low"
|
||||
// (Because yes, I did myself too a week after writing it)
|
||||
// char.IsLowSurrogate(_text[_cursorPosition]) means "is the cursor between a surrogate pair"
|
||||
// because we ALREADY moved.
|
||||
if (char.IsLowSurrogate(Index(rope, index)))
|
||||
index += 1;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given rope is either null or empty (length 0).
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public static bool IsNullOrEmpty([NotNullWhen(false)] Node? rope)
|
||||
{
|
||||
if (rope == null)
|
||||
return true;
|
||||
|
||||
return CalcTotalLength(rope) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A nope in a rope. This is either a <see cref="Leaf"/> or a <see cref="Branch"/>.
|
||||
/// </summary>
|
||||
public abstract class Node
|
||||
{
|
||||
public abstract long Weight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The depth of the deepest leaf in this node tree. A leaf has depth 0, and a branch one above 1, etc...
|
||||
/// </summary>
|
||||
public abstract short Depth { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A leaf contains a string of text.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("W: {Weight}, Text: {Text}")]
|
||||
public sealed class Leaf : Node
|
||||
{
|
||||
public static readonly Leaf Empty = new("");
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public Leaf(string text)
|
||||
{
|
||||
Text = text;
|
||||
}
|
||||
|
||||
public override long Weight => Text.Length;
|
||||
public override short Depth => 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A branch contains other nodes to the left and right.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("W: {Weight}")]
|
||||
public sealed class Branch : Node
|
||||
{
|
||||
public Node Left { get; }
|
||||
public Node? Right { get; }
|
||||
public override long Weight { get; }
|
||||
public override short Depth { get; }
|
||||
|
||||
public Branch(Node left, Node? right)
|
||||
{
|
||||
Left = left;
|
||||
Right = right;
|
||||
Weight = CalcTotalLength(left);
|
||||
Depth = checked((short)(Math.Max(left.Depth, right?.Depth ?? 0) + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user