Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propagate cancellation better #68

Merged
merged 1 commit into from
Jul 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions Core/Completion/XmlCompletionTriggering.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System;
using System.Diagnostics;
using System.Threading;

using MonoDevelop.Xml.Dom;
using MonoDevelop.Xml.Parser;

Expand All @@ -12,10 +14,14 @@ class XmlCompletionTriggering
{
public static XmlCompletionTrigger GetTrigger (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter) => GetTriggerAndIncompleteSpan (parser, reason, typedCharacter).kind;

public static (XmlCompletionTrigger kind, int spanStart, int spanLength) GetTriggerAndSpan (XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter, ITextSource spanReadForwardTextSource)
public static (XmlCompletionTrigger kind, int spanStart, int spanLength) GetTriggerAndSpan (
XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter, ITextSource spanReadForwardTextSource,
int maximumReadahead = XmlParserTextSourceExtensions.DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
var result = GetTriggerAndIncompleteSpan (parser, reason, typedCharacter);
var spanLength = GetReadForwardLength (spanReadForwardTextSource, parser, result.spanStart, result.spanReadForward);
if (!TryGetReadForwardLength (spanReadForwardTextSource, parser, result.spanStart, result.spanReadForward, out int spanLength, maximumReadahead, cancellationToken)) {
spanLength = parser.Position - result.spanStart;
}
return (result.kind, result.spanStart, spanLength);
}

Expand Down Expand Up @@ -144,16 +150,16 @@ enum XmlReadForward
}

//TODO: support the other readforward types
static int GetReadForwardLength (ITextSource textSource, XmlSpineParser spine, int spanStart, XmlReadForward spanReadForward)
static bool TryGetReadForwardLength (ITextSource textSource, XmlSpineParser spine, int spanStart, XmlReadForward spanReadForward, out int length, int maximumReadahead, CancellationToken cancellationToken)
{
int triggerPosition = spine.Position;
switch (spanReadForward) {
case XmlReadForward.XmlName:
return textSource.GetXNameLengthAtPosition (spanStart, triggerPosition);
return textSource.TryGetXNameLengthAtPosition (spanStart, triggerPosition, out length, maximumReadahead, cancellationToken);

case XmlReadForward.AttributeValue:
var attributeDelimiter = spine.GetAttributeValueDelimiter () ?? '\0';
return textSource.GetAttributeValueLengthAtPosition (attributeDelimiter, spanStart, triggerPosition);
return textSource.TryGetAttributeValueLengthAtPosition (attributeDelimiter, spanStart, triggerPosition, out length, maximumReadahead, cancellationToken);

case XmlReadForward.TagStart:
int existingLength = triggerPosition - spanStart;
Expand All @@ -166,17 +172,23 @@ static int GetReadForwardLength (ITextSource textSource, XmlSpineParser spine, i
break;
}
if (specialTagIndex + 1 == specialTag.Length) {
return specialTag.Length;
length = specialTag.Length;
return true;
}
if (cancellationToken.IsCancellationRequested) {
length = 0;
return false;
}
}
}
}
return textSource.GetXNameLengthAtPosition (spanStart, triggerPosition);
return textSource.TryGetXNameLengthAtPosition (spanStart, triggerPosition, out length, maximumReadahead, cancellationToken);

case XmlReadForward.DocType:
case XmlReadForward.Entity:
case XmlReadForward.None:
return triggerPosition - spanStart;
length = 0;
return false;
default:
throw new ArgumentException ("Unsupported XmlReadForward value", nameof (spanReadForward));
}
Expand Down
159 changes: 128 additions & 31 deletions Core/Parser/XmlParserTextSourceExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,78 +4,111 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;

using MonoDevelop.Xml.Dom;

namespace MonoDevelop.Xml.Parser
{
public static class XmlParserTextSourceExtensions
{
const int DEFAULT_READAHEAD_LIMIT = 5000;
internal const int DEFAULT_READAHEAD_LIMIT = 5000;

/// <summary>
/// Gets the XML name at the parser's position.
/// </summary>
/// <param name="spine">A spine parser. It will not be modified.</param>
/// <param name="text">The text snapshot corresponding to the parser.</param>
/// <returns></returns>
public static XName GetCompleteName (this XmlSpineParser spine, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
public static bool TryGetCompleteName (this XmlSpineParser spine, ITextSource text, out XName xname, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
Debug.Assert (spine.CurrentState is XmlNameState);

int start = spine.Position - spine.CurrentStateLength;
var length = text.GetXNameLengthAtPosition (start, spine.Position, maximumReadahead);
if (!text.TryGetXNameLengthAtPosition (start, spine.Position, out int length, maximumReadahead, cancellationToken)) {
xname = XName.Empty;
return false;
}

string name = text.GetText (start, length);

int i = name.IndexOf (':');
if (i < 0) {
return new XName (name);
xname = new XName (name);
return true;
} else {
return new XName (name.Substring (0, i), name.Substring (i + 1));
xname = new XName (name.Substring (0, i), name.Substring (i + 1));
return true;
}
}

public static int GetXNameLengthAtPosition (this ITextSource text, int nameStartPosition, int currentPosition, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
public static bool TryGetXNameLengthAtPosition (this ITextSource text, int nameStartPosition, int currentPosition, out int xnameLength, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
int limit = Math.Min (text.Length, currentPosition + maximumReadahead);
int readaheadLimit = currentPosition + maximumReadahead;
int limit = Math.Min (text.Length, readaheadLimit);

//try to find the end of the name, but don't go too far
for (; currentPosition < limit; currentPosition++) {
char c = text[currentPosition];
if (!XmlChar.IsNameChar (c)) {
break;
}
if (cancellationToken.IsCancellationRequested) {
xnameLength = 0;
return false;
}
}

if (currentPosition + 1 == readaheadLimit) {
xnameLength = 0;
return false;
}

return currentPosition - nameStartPosition;
xnameLength = currentPosition - nameStartPosition;
return true;
}

public static int GetAttributeValueLengthAtPosition (this ITextSource text, char delimiter, int attributeStartPosition, int currentPosition, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
public static bool TryGetAttributeValueLengthAtPosition (this ITextSource text, char delimiter, int attributeStartPosition, int currentPosition, out int attributeValueLength, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
int limit = Math.Min (text.Length, currentPosition + maximumReadahead);
int readaheadLimit = currentPosition + maximumReadahead;
int limit = Math.Min (text.Length, readaheadLimit);

//try to find the end of the name, but don't go too far
for (; currentPosition < limit; currentPosition++) {
char c = text[currentPosition];
if (XmlChar.IsInvalid (c) || c == '<') {
return currentPosition - attributeStartPosition;
attributeValueLength = currentPosition - attributeStartPosition - 1;
return true;
}
switch (delimiter) {
case '\'':
case '"':
if (c == delimiter) {
return currentPosition - attributeStartPosition;
attributeValueLength = currentPosition - attributeStartPosition;
return true;
}
break;
default:
if (XmlChar.IsWhitespace (c)) {
return currentPosition - attributeStartPosition;
attributeValueLength = currentPosition - attributeStartPosition;
return true;
}
break;
}
if (cancellationToken.IsCancellationRequested) {
attributeValueLength = 0;
return false;
}
}

return currentPosition - attributeStartPosition;
if (currentPosition + 1 == readaheadLimit) {
attributeValueLength = 0;
return false;
}

attributeValueLength = currentPosition - attributeStartPosition;
return true;
}

/// <summary>
Expand All @@ -86,17 +119,19 @@ public static int GetAttributeValueLengthAtPosition (this ITextSource text, char
/// <param name="text">The text snapshot corresponding to the parser.</param>
/// <param name="maximumReadahead">Maximum number of characters to advance before giving up.</param>
/// <returns>Whether the object was successfully completed</returns>
public static bool AdvanceUntilClosed (this XmlSpineParser parser, XObject ob, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
public static bool AdvanceUntilClosed (this XmlSpineParser parser, XObject ob, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
var el = ob as XElement;
if (el == null) {
return AdvanceUntilEnded (parser, ob, text, maximumReadahead);
return AdvanceUntilEnded (parser, ob, text, maximumReadahead, cancellationToken);
}

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

var startingDepth = parser.Spine.Count;

var end = Math.Min (text.Length - parser.Position, maximumReadahead) + parser.Position;
while (parser.Position < end) {
while (parser.Position < limit) {
parser.Push (text[parser.Position]);
if (el.IsClosed) {
return true;
Expand All @@ -105,7 +140,11 @@ public static bool AdvanceUntilClosed (this XmlSpineParser parser, XObject ob, I
if (parser.Spine.Count < startingDepth - 1) {
return false;
}
if (cancellationToken.IsCancellationRequested) {
return false;
}
}

return false;
}

Expand All @@ -117,23 +156,63 @@ public static bool AdvanceUntilClosed (this XmlSpineParser parser, XObject ob, I
/// <param name="text">The text snapshot corresponding to the parser.</param>
/// <param name="maximumReadahead">Maximum number of characters to advance before giving up.</param>
/// <returns>Whether the object was successfully completed</returns>
public static bool AdvanceUntilEnded (this XmlSpineParser parser, XObject ob, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
public static bool AdvanceUntilEnded (this XmlSpineParser parser, XObject ob, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
var startingDepth = parser.Spine.Count;

var end = Math.Min (text.Length - parser.Position, maximumReadahead) + parser.Position;
while (parser.Position < end) {
int readaheadLimit = parser.Position + maximumReadahead;
int limit = Math.Min (text.Length, readaheadLimit);

while (parser.Position < limit) {
parser.Push (text[parser.Position]);
if (ob.IsEnded) {
return true;
}
if (parser.Spine.Count < startingDepth) {
return false;
}
if (cancellationToken.IsCancellationRequested) {
return false;
}
}

// if at end of document, consider text nodes to be ended anyways
if (parser.Position == text.Length && ob is XText xt) {
xt.End (text.GetTextBetween (xt.Span.Start, text.Length));
return true;
}

if (parser.Position + 1 == readaheadLimit) {
return false;
}

return false;
}

/// <summary>
/// Advances the parser until the specified condition is met or the end of the line is reached.
/// </summary>
public static bool AdvanceParserUntilConditionOrEol (this XmlSpineParser parser, ITextSource text, Func<XmlParser, bool> condition, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
if (parser.Position == text.Length) {
return true;
}

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

while (parser.Position < limit) {
char c = text[parser.Position];
if (c == '\r' || c == '\n') {
return true;
}
parser.Push (c);
if (condition (parser)) {
return true;
}
if (cancellationToken.IsCancellationRequested) {
return false;
}
}
return false;
}
Expand All @@ -143,19 +222,23 @@ public static bool AdvanceUntilEnded (this XmlSpineParser parser, XObject ob, IT
/// </summary>
/// <param name="parser">A spine parser. Its state will not be modified.</param>
/// <param name="text">The text snapshot corresponding to the parser.</param>
public static List<XObject> GetNodePath (this XmlSpineParser parser, ITextSource text)
public static bool TryGetNodePath (this XmlSpineParser parser, ITextSource text, [NotNullWhen (true)] out List<XObject>? nodePath, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
var path = parser.Spine.ToNodePath ();

//complete last node's name without altering the parser state
int lastIdx = path.Count - 1;
if (parser.CurrentState is XmlNameState && path[lastIdx] is INamedXObject) {
XName completeName = GetCompleteName (parser, text);
if (!TryGetCompleteName (parser, text, out XName completeName, maximumReadahead, cancellationToken)) {
nodePath = null;
return false;
}
var obj = path[lastIdx] = path[lastIdx].ShallowCopy ();
((INamedXObject)obj).Name = completeName;
}

return path;
nodePath = path;
return true;
}

/// <summary>
Expand All @@ -164,24 +247,34 @@ public static List<XObject> GetNodePath (this XmlSpineParser parser, ITextSource
/// <param name="parser">A spine parser. Its state will be modified.</param>
/// <param name="text">The text snapshot corresponding to the parser.</param>
/// <returns></returns>
public static List<XObject> AdvanceToNodeEndAndGetNodePath (this XmlSpineParser parser, ITextSource text, int maximumReadahead = DEFAULT_READAHEAD_LIMIT)
public static bool TryAdvanceToNodeEndAndGetNodePath (this XmlSpineParser parser, ITextSource text, [NotNullWhen (true)] out List<XObject>? nodePath, int maximumReadahead = DEFAULT_READAHEAD_LIMIT, CancellationToken cancellationToken = default)
{
var context = parser.GetContext ();

int startOffset = parser.Position;
int startDepth = parser.Spine.Count;

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

//if in potential start of a state, advance into the next state
var end = Math.Min (text.Length - context.Position, maximumReadahead) + context.Position;
if (parser.Position < end && (XmlRootState.IsNotFree (context) || (context.CurrentState is XmlRootState && text[parser.Position] == '<'))) {
if (parser.Position < limit && (XmlRootState.IsNotFree (context) || (context.CurrentState is XmlRootState && text[parser.Position] == '<'))) {
do {
parser.Push (text[parser.Position]);
} while (parser.Position < end && XmlRootState.IsNotFree (context));
if (cancellationToken.IsCancellationRequested) {
nodePath = null;
return false;
}
} while (parser.Position < limit && XmlRootState.IsNotFree (context));

//if it transitioned to another state, eat until we get a new node on the stack
if (parser.Position < end && !(context.CurrentState is XmlRootState) && context.Nodes.Count <= startDepth) {
if (parser.Position < limit && !(context.CurrentState is XmlRootState) && context.Nodes.Count <= startDepth) {
parser.Push (text[parser.Position]);
}
if (cancellationToken.IsCancellationRequested) {
nodePath = null;
return false;
}
}

var path = parser.Spine.ToNodePath ();
Expand All @@ -190,15 +283,19 @@ public static List<XObject> AdvanceToNodeEndAndGetNodePath (this XmlSpineParser
if (path.Count > 0) {
var leaf = path[path.Count-1];
if (!(leaf is XDocument)) {
AdvanceUntilEnded (parser, leaf, text, maximumReadahead - (parser.Position - startOffset));
if (!AdvanceUntilEnded (parser, leaf, text, maximumReadahead - (parser.Position - startOffset))) {
nodePath = null;
return false;
}
}
//the leaf node might have a child that's a better match for the offset
if (leaf is XContainer c && c.FindAtOffset (startOffset) is XObject o) {
path.Add (o);
}
}

return path;
nodePath = path;
return true;
}

static List<XObject> ToNodePath (this NodeStack stack)
Expand Down
Loading