Skip to content

Commit

Permalink
Implement readahead in GetIncompleteValue
Browse files Browse the repository at this point in the history
  • Loading branch information
mhutch committed Apr 30, 2024
1 parent 96cd6b0 commit 3760157
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 29 deletions.
93 changes: 92 additions & 1 deletion Core/Parser/XmlParserTextSourceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ public static bool TryAdvanceToNodeEndAndGetNodePath (this XmlSpineParser parser
if (path.Count > 0) {
var leaf = path[path.Count-1];
if (!(leaf is XDocument)) {
if (!AdvanceUntilEnded (parser, leaf, text, maximumReadahead - (parser.Position - startOffset))) {
if (!AdvanceUntilEnded (parser, leaf, text, maximumReadahead - (parser.Position - startOffset), cancellationToken)) {
nodePath = null;
return false;
}
Expand All @@ -300,5 +300,96 @@ public static bool TryAdvanceToNodeEndAndGetNodePath (this XmlSpineParser parser
nodePath = path;
return true;
}

/// <summary>
/// If the parser is within an attribute value or XText node, determine the full value and span of that value.
/// </summary>
/// <param name="parser">A spine parser. Its position will not be modified.</param>
/// <param name="text">The text snapshot corresponding to the parser.</param>
/// <param name="value">The full value of the attribute or text, or as much as could be recovered before hitting the readahead limit.</param>
/// <param name="valueSpan">The span of the <paramref name="value"/>.</param>
/// <param name="stopAtLineBreak">Whether to truncate the value upon reaching a line break.</param>
/// <param name="maximumReadahead">Maximum number of characters to advance before giving up.</param>
/// <returns>Whether the parser is in a value state and the full value could be read. If <c>false</c>, and the parser was in a value state, the <paramref name="value"/> may be non-null but incomplete.</returns>
public static bool TryGetIncompleteValue (
this XmlSpineParser parser, ITextSource text, [NotNullWhen (true)] out string? value, [NotNullWhen (true)] out TextSpan? valueSpan,
bool stopAtLineBreak = false, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
if (parser.IsInAttributeValue ()) {
int length = parser.CurrentStateLength;
var cloneParser = parser.Clone ();

bool success = cloneParser.TryAdvanceUntilStateChange (text, stopAtLineBreak, out var deepestNode, maximumReadahead, cancellationToken);

if (cancellationToken.IsCancellationRequested) {
value = null;
valueSpan = null;
return false;
}

// this should always succeed, as a value state can only begin when an attribute state is on the stack already
var attributeNode = (XAttribute)deepestNode;

if (success) {
valueSpan = attributeNode.ValueSpan!.Value;
value = attributeNode.Value ?? "";
} else {
value = cloneParser.GetContext().KeywordBuilder.ToString ();
valueSpan = new TextSpan (cloneParser.Position - value.Length, value.Length);
}
return success;
}

if (parser.IsInText ()) {
int length = parser.CurrentStateLength;
var cloneParser = parser.Clone ();
bool success = cloneParser.TryAdvanceUntilStateChange (text, stopAtLineBreak, out var deepestNode, maximumReadahead, cancellationToken);

// deepestNode may not be XText if it was at the start of the state and no chars were pushed due to cancellation
if (cancellationToken.IsCancellationRequested || deepestNode is not XText textNode) {
value = null;
valueSpan = null;
return false;
}

// if we did not reach the end of the node, force it to end anyways
if (!success) {
cloneParser.Push ('<');
}

valueSpan = textNode.Span;
value = textNode.Text;

return success;
}

value = null;
valueSpan = null;
return false;
}

static bool TryAdvanceUntilStateChange (this XmlSpineParser parser, ITextSource text, bool stopAtLineBreak, out XObject captureDeepestNode, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
var nodeStack = parser.GetContext ().Nodes;
captureDeepestNode = nodeStack.Peek ();
int deepestStack = nodeStack.Count;

int readaheadLimit = parser.Position + maximumReadahead;
int limit = Math.Min (text.Length, readaheadLimit);
var startingState = parser.CurrentState;

while (parser.Position < limit && parser.CurrentState == startingState && !cancellationToken.IsCancellationRequested) {
char c = text[parser.Position];
if (stopAtLineBreak && (c == '\n' || c == '\r')) {
break;
}
parser.Push (c);
if (nodeStack.Count > deepestStack) {
captureDeepestNode = nodeStack.Peek ();
}
}

return parser.CurrentState != startingState;
}
}
}
60 changes: 47 additions & 13 deletions Editor.Tests/Parser/SpineParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,57 @@ namespace MonoDevelop.Xml.Tests.Parser;
[TestFixture]
public class SpineParserTests : XmlEditorTest
{
[Test]
[TestCase ("<a><b>", "")]
[TestCase ("<a><b>foo", "foo")]
[TestCase ("<a><b> foo", "foo")]
[TestCase ("<a><b bar='", "")]
[TestCase ("<a><b bar='xyz", "xyz")]
[TestCase ("<a><b bar=' xyz", " xyz")]
public void TestGetIncompleteValue (string doc, string expected)
[TestCase ("<a><b>|", null)]
[TestCase ("<a><b>fo|o", "foo")]
[TestCase ("<a><b>foo|_123", "foo_123")]
[TestCase ("<a><b>fo|o.abc", "foo.abc")]
[TestCase ("<a><b>fo|o.abc ", "foo.abc")]
[TestCase ("<a><b>fo|o.abc\\", "foo.abc\\")]
[TestCase ("<a><b>fo|o.abc\\qwerty", "foo.abc\\qwerty")]
[TestCase ("<a><b>fo|o.abc/", "foo.abc/")]
[TestCase ("<a><b>fo|o.abc#123", "foo.abc#123")]
[TestCase ("<a><b>fo|o.abc<", "foo.abc")]
[TestCase ("<a><b>fo|o.abc <", "foo.abc")]
[TestCase ("<a><b> fo|o", "foo")]
[TestCase ("<a><b> foo|123 ", "foo123")]
[TestCase ("<a><b> hello\n foo|123 \n bye", "hello\n foo123 \n bye")]
[TestCase ("<a><b> hello\n foo|123 \n bye", "hello\n foo123", false, true)]
[TestCase ("<a><b> hello\n foo|123 \n bye", "foo123 \n bye", true, false)]
[TestCase ("<a><b bar='|", "")]
[TestCase ("<a><b bar='|xyz", "xyz")]
[TestCase ("<a><b bar=' x|yz", " xyz")]
[TestCase ("<a><b bar=' xy|zabc ", " xyzabc ")]
[TestCase ("<a><b bar=' xy|zabc\\", " xyzabc\\")]
[TestCase ("<a><b bar=' xy|z.abc", " xyz.abc")]
[TestCase ("<a><b bar='xy|z_ ", "xyz_ ")]
[TestCase ("<a><b bar='xy|z_a", "xyz_a")]
[TestCase ("<a><b bar='xyz|_a", "xyz_a")]
[TestCase ("<a><b bar='xyz|_a'", "xyz_a")]
[TestCase ("<a><b bar=\"xyz|_a\"", "xyz_a")]
[TestCase ("<a><b bar=xy|z ", "xyz")]
public void TestGetIncompleteValue (string documentWithMarkers, string expected, bool startAtLineBreak = false, bool stopAtLineBreak = false)
{
var buffer = CreateTextBuffer (doc);
var snapshot = buffer.CurrentSnapshot;
var caretPoint = new SnapshotPoint (snapshot, snapshot.Length);
char caretMarker = '|';
var doc = TextWithMarkers.Parse (documentWithMarkers, caretMarker);
var caretPos = doc.GetMarkedPosition (caretMarker);

var buffer = CreateTextBuffer (doc.Text);
var snapshot = buffer.CurrentSnapshot;
var caretPoint = new SnapshotPoint (snapshot, caretPos);
var spine = GetParser (buffer).GetSpineParser (caretPoint);
var actual = spine.GetIncompleteValue (snapshot);

Assert.AreEqual (expected, actual);
spine.TryGetIncompleteValue (snapshot, out string actualValue, out SnapshotSpan? maybeExpressionSpan, startAtLineBreak, stopAtLineBreak);

if (expected is null) {
Assert.IsNull (actualValue);
return;
}

Assert.AreEqual (expected, actualValue);

var valueSpanText = maybeExpressionSpan.Value.GetText ();
Assert.AreEqual (expected, valueSpanText);

}

[Test]
Expand Down
58 changes: 43 additions & 15 deletions Editor/XmlParserSnapshotExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,26 +77,54 @@ public static bool TryAdvanceToNodeEndAndGetNodePath (this XmlSpineParser parser
=> parser.TryAdvanceToNodeEndAndGetNodePath (new SnapshotTextSource (snapshot), out nodePath, maximumReadahead, cancellationToken);


public static string GetIncompleteValue (this XmlSpineParser spineAtCaret, ITextSnapshot snapshot)
/// <summary>
/// If the parser is within an attribute value or XText node, determine the full value and span of that value.
/// </summary>
/// <param name="parser">A spine parser. Its position will not be modified.</param>
/// <param name="text">The text snapshot corresponding to the parser.</param>
/// <param name="value">The full value of the attribute or text, or as much as could be recovered before hitting the readahead limit.</param>
/// <param name="valueSpan">The span of the <paramref name="value"/>.</param>
/// <param name="startAtLineBreak">Whether to truncate the value so that it does not start before the beginning of line that contains the parser position.</param>
/// <param name="stopAtLineBreak">Whether to truncate the value upon reaching a line break.</param>
/// <param name="maximumReadahead">Maximum number of characters to advance before giving up.</param>
/// <returns>Whether the parser is in a value state and the full value could be read. If <c>false</c>, and the parser was in a value state, the <paramref name="value"/> may be non-null but incomplete.</returns>

public static bool TryGetIncompleteValue (
this XmlSpineParser parser, ITextSnapshot snapshot, [NotNullWhen (true)] out string? value, [NotNullWhen (true)] out SnapshotSpan? valueSpan,
bool startAtLineBreak = false, bool stopAtLineBreak = false, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
int caretPosition = spineAtCaret.Position;
var node = spineAtCaret.Spine.Peek ();

int valueStart;
if (node is XText t) {
valueStart = t.Span.Start;
} else if (node is XElement el && el.IsEnded) {
valueStart = el.Span.End;
} else {
int caretPosition = parser.Position;

bool isTextNode = parser.IsInText ();

var success = parser.TryGetIncompleteValue (new SnapshotTextSource (snapshot), out value, out var textSpan, stopAtLineBreak, maximumReadahead, cancellationToken);

if (textSpan is not TextSpan span || value is null) {
valueSpan = null;
return false;
}

if (startAtLineBreak) {
int lineStart = snapshot.GetLineFromPosition (caretPosition).Start.Position;
valueStart = spineAtCaret.Position - spineAtCaret.CurrentStateLength;
if (spineAtCaret.GetAttributeValueDelimiter ().HasValue) {
valueStart += 1;
if (span.ContainsOuter (lineStart)) {
valueSpan = new SnapshotSpan (snapshot, lineStart, span.Length - (lineStart - span.Start));
var valueRaw = valueSpan.Value.GetText ();
// trim any leading whitespace if it's a text node
// since text nodes already ignore surrounding whitespace
if (isTextNode) {
value = valueRaw.TrimStart ();
int trimmed = valueRaw.Length - value.Length;
if (trimmed > 0) {
valueSpan = new SnapshotSpan (snapshot, valueSpan.Value.Start + trimmed, valueSpan.Value.Length - trimmed);
}
}

return success;
}
valueStart = Math.Min (Math.Max (valueStart, lineStart), caretPosition);
}

return snapshot.GetText (valueStart, caretPosition - valueStart);
valueSpan = new SnapshotSpan (snapshot, span.Start, span.Length);
return success;
}

public static string GetAttributeOrElementValueToCaret (this XmlSpineParser spineAtCaret, SnapshotPoint caretPosition)
Expand Down

0 comments on commit 3760157

Please sign in to comment.