From 37601578d409175fb4ba15bac52d48695363eb1f Mon Sep 17 00:00:00 2001 From: Mikayla Hutchinson Date: Tue, 30 Apr 2024 00:27:50 -0400 Subject: [PATCH] Implement readahead in GetIncompleteValue --- Core/Parser/XmlParserTextSourceExtensions.cs | 93 +++++++++++++++++++- Editor.Tests/Parser/SpineParserTests.cs | 60 ++++++++++--- Editor/XmlParserSnapshotExtensions.cs | 58 ++++++++---- 3 files changed, 182 insertions(+), 29 deletions(-) diff --git a/Core/Parser/XmlParserTextSourceExtensions.cs b/Core/Parser/XmlParserTextSourceExtensions.cs index 167d813..76a450d 100644 --- a/Core/Parser/XmlParserTextSourceExtensions.cs +++ b/Core/Parser/XmlParserTextSourceExtensions.cs @@ -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; } @@ -300,5 +300,96 @@ public static bool TryAdvanceToNodeEndAndGetNodePath (this XmlSpineParser parser nodePath = path; return true; } + + /// + /// If the parser is within an attribute value or XText node, determine the full value and span of that value. + /// + /// A spine parser. Its position will not be modified. + /// The text snapshot corresponding to the parser. + /// The full value of the attribute or text, or as much as could be recovered before hitting the readahead limit. + /// The span of the . + /// Whether to truncate the value upon reaching a line break. + /// Maximum number of characters to advance before giving up. + /// Whether the parser is in a value state and the full value could be read. If false, and the parser was in a value state, the may be non-null but incomplete. + 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; + } } } diff --git a/Editor.Tests/Parser/SpineParserTests.cs b/Editor.Tests/Parser/SpineParserTests.cs index 73faeeb..fdf1c8b 100644 --- a/Editor.Tests/Parser/SpineParserTests.cs +++ b/Editor.Tests/Parser/SpineParserTests.cs @@ -19,23 +19,57 @@ namespace MonoDevelop.Xml.Tests.Parser; [TestFixture] public class SpineParserTests : XmlEditorTest { - [Test] - [TestCase ("", "")] - [TestCase ("foo", "foo")] - [TestCase (" foo", "foo")] - [TestCase (" parser.TryAdvanceToNodeEndAndGetNodePath (new SnapshotTextSource (snapshot), out nodePath, maximumReadahead, cancellationToken); - public static string GetIncompleteValue (this XmlSpineParser spineAtCaret, ITextSnapshot snapshot) + /// + /// If the parser is within an attribute value or XText node, determine the full value and span of that value. + /// + /// A spine parser. Its position will not be modified. + /// The text snapshot corresponding to the parser. + /// The full value of the attribute or text, or as much as could be recovered before hitting the readahead limit. + /// The span of the . + /// Whether to truncate the value so that it does not start before the beginning of line that contains the parser position. + /// Whether to truncate the value upon reaching a line break. + /// Maximum number of characters to advance before giving up. + /// Whether the parser is in a value state and the full value could be read. If false, and the parser was in a value state, the may be non-null but incomplete. + + 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)