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

Improve triggering #65

Merged
merged 6 commits into from
Jul 7, 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
150 changes: 88 additions & 62 deletions Core.Tests/Completion/CompletionTriggerTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.IO;
using System;

using MonoDevelop.Xml.Editor.Completion;
using MonoDevelop.Xml.Parser;
using MonoDevelop.Xml.Tests.Parser;
using NUnit.Framework;
using NUnit.Framework.Internal;

Expand All @@ -14,91 +14,117 @@ namespace MonoDevelop.Xml.Tests.Completion
public class CompletionTriggerTests
{
[Test]
// params are: document text, typedChar, trigger reason, trigger result, length
// params are: document text, typedChar, trigger reason, trigger result
// typedChar, trigger reason and length can be omitted
// typedChar defaults to \0
// trigger reason defaults to insertion if typedChar is non-null, else invocation
// length defaults to 0
// if typedChar is provided, it's added to the document text
[TestCase ("", XmlCompletionTrigger.ElementWithBracket)]
[TestCase("<", XmlCompletionTrigger.Element)]
// the document text may use the following optional marker chars:
// | - cursor position, if not provided, defaults to end of document
// ^ - span start. if not provided, defaults to cursor position
// $ - span end. if not provided, defaults to cursor position, unless trigger reason is 'insertion', in which case it defaults to cursor position + 1
[TestCase ("", XmlCompletionTrigger.ElementValue)]
[TestCase("^<", XmlCompletionTrigger.Tag)]
[TestCase("", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)]
[TestCase("<", 'a', XmlCompletionTrigger.Element, 1)]
[TestCase("<abc", XmlCompletionTrigger.Element, 3)]
[TestCase("<^a|", XmlTriggerReason.TypedChar, XmlCompletionTrigger.ElementName)]
[TestCase("<^abc|de$ ", XmlCompletionTrigger.ElementName)]
[TestCase("<abc", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)]
[TestCase("<", XmlTriggerReason.Backspace, XmlCompletionTrigger.Element)]
[TestCase ("", '<', XmlCompletionTrigger.Element)]
[TestCase ("sometext", '<', XmlCompletionTrigger.Element)]
[TestCase("^<", XmlTriggerReason.Backspace, XmlCompletionTrigger.Tag)]
[TestCase ("^<", XmlTriggerReason.TypedChar, XmlCompletionTrigger.Tag)]
[TestCase ("sometext^<", XmlTriggerReason.TypedChar, XmlCompletionTrigger.Tag)]
[TestCase ("sometext", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)]
[TestCase ("", 'a', XmlCompletionTrigger.None)]
[TestCase("\"", '"', XmlCompletionTrigger.None)]
[TestCase("<foo", '"', XmlCompletionTrigger.None)]
[TestCase ("<foo", ' ', XmlCompletionTrigger.Attribute)]
[TestCase ("<foo bar='1' ", ' ', XmlCompletionTrigger.Attribute)]
[TestCase ("<foo ", XmlCompletionTrigger.Attribute)]
[TestCase ("<foo a", XmlCompletionTrigger.Attribute, 1)]
[TestCase ("<foo bar", XmlCompletionTrigger.Attribute, 3)]
[TestCase ("", '&', XmlCompletionTrigger.Entity)]
[TestCase ("&", XmlCompletionTrigger.Entity)]
[TestCase ("&", 'a', XmlCompletionTrigger.None)]
[TestCase ("&", XmlTriggerReason.Backspace, XmlCompletionTrigger.Entity)]
[TestCase ("&blah", XmlCompletionTrigger.Entity, 4)]
[TestCase ("a", XmlTriggerReason.TypedChar, XmlCompletionTrigger.None)]
[TestCase("\"\"", XmlTriggerReason.TypedChar, XmlCompletionTrigger.None)]
[TestCase("<foo\"", XmlTriggerReason.TypedChar, XmlCompletionTrigger.None)]
[TestCase("^<|foo$ bar", XmlTriggerReason.Invocation, XmlCompletionTrigger.Tag)]
[TestCase("^<|!--$ bar", XmlTriggerReason.Invocation, XmlCompletionTrigger.Tag)]
[TestCase("^<|![CDATA[$ bar", XmlTriggerReason.Invocation, XmlCompletionTrigger.Tag)]
[TestCase ("<foo ^|", XmlTriggerReason.TypedChar, XmlCompletionTrigger.AttributeName)]
[TestCase ("<foo bar='1' ^| ", XmlTriggerReason.TypedChar, XmlCompletionTrigger.AttributeName)]
[TestCase ("<foo ^| ", XmlCompletionTrigger.AttributeName)]
[TestCase ("<foo ^a|bc$ ", XmlCompletionTrigger.AttributeName)]
[TestCase ("<foo ^bar|baz$=", XmlCompletionTrigger.AttributeName)]
[TestCase ("^&", XmlTriggerReason.TypedChar, XmlCompletionTrigger.Entity)]
[TestCase ("^&", XmlCompletionTrigger.Entity)]
[TestCase ("&a", XmlTriggerReason.TypedChar, XmlCompletionTrigger.Entity)]
[TestCase ("^&", XmlTriggerReason.Backspace, XmlCompletionTrigger.Entity)]
[TestCase ("^&blah", XmlCompletionTrigger.Entity)]
[TestCase ("&blah", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)]
[TestCase ("<foo ", '&', XmlCompletionTrigger.None)]
[TestCase ("<foo bar='", '&', XmlCompletionTrigger.Entity)]
[TestCase ("<", '!', XmlCompletionTrigger.DeclarationOrCDataOrComment, 2)]
[TestCase ("<!", XmlCompletionTrigger.DeclarationOrCDataOrComment, 2)]
[TestCase ("<!DOCTYPE foo", XmlCompletionTrigger.DocType, 13)]
[TestCase ("<!DOC", XmlCompletionTrigger.DocType, 5)]
[TestCase ("<foo &", XmlTriggerReason.TypedChar, XmlCompletionTrigger.None)]
[TestCase ("<foo bar='&", XmlTriggerReason.TypedChar, XmlCompletionTrigger.Entity)]
[TestCase ("^<!", XmlTriggerReason.TypedChar, XmlCompletionTrigger.DeclarationOrCDataOrComment)]
[TestCase ("^<!", XmlCompletionTrigger.DeclarationOrCDataOrComment)]
[TestCase ("^<!DOCTYPE foo", XmlCompletionTrigger.DocType)]
[TestCase ("^<!DOC", XmlCompletionTrigger.DocType)]
[TestCase ("<foo bar=\"", XmlCompletionTrigger.AttributeValue)]
[TestCase ("<foo bar='", XmlCompletionTrigger.AttributeValue)]
[TestCase ("<foo bar='", XmlTriggerReason.Backspace, XmlCompletionTrigger.AttributeValue)]
[TestCase ("<foo bar='abc", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)]
[TestCase ("<foo bar=", '"', XmlCompletionTrigger.AttributeValue)]
[TestCase ("<foo bar=", '\'', XmlCompletionTrigger.AttributeValue)]
[TestCase ("<foo bar='wxyz", XmlCompletionTrigger.AttributeValue, 4)]
[TestCase ("<foo bar=\"^|", XmlTriggerReason.TypedChar, XmlCompletionTrigger.AttributeValue)]
[TestCase ("<foo bar='1' ^| ", XmlTriggerReason.TypedChar, XmlCompletionTrigger.AttributeName)]
[TestCase ("<foo bar='^wxyz|a12$'", XmlCompletionTrigger.AttributeValue)]
[TestCase ("<foo bar=wxyz", XmlCompletionTrigger.None)]
[TestCase ("<foo bar=wxyz", XmlTriggerReason.Backspace, XmlCompletionTrigger.None)]
[TestCase ("<foo bar=wxyz", "'", XmlCompletionTrigger.None)]
[TestCase ("<foo bar=wxyz'", XmlTriggerReason.TypedChar, XmlCompletionTrigger.None)]
public void TriggerTests (object[] args)
{
//first arg is the doc
string doc = (string)args[0];
int argIdx = 0;

XmlTriggerReason reason;
char typedChar;

//next arg can be typed char or a trigger reason
//this would make a nice switch expression w/c#8
if (args[1] is XmlTriggerReason r) {
reason = r;
typedChar = '\0';
} else if (args[1] is char c) {
reason = XmlTriggerReason.TypedChar;
typedChar = c;
//first arg is the doc
if (args.Length == 0 || args[argIdx++] is not string doc) {
throw new ArgumentException ("First argument must be an string");
}
if (argIdx < args.Length && args[argIdx] is XmlTriggerReason triggerReason) {
argIdx++;
} else {
reason = XmlTriggerReason.Invocation;
typedChar = '\0';
triggerReason = XmlTriggerReason.Invocation;
}
if (argIdx != args.Length - 1 || args[argIdx] is not XmlCompletionTrigger expectedTrigger) {
throw new ArgumentException ("Last argument must be an XmlCompletionTrigger");
}

//expected trigger will be last unless length is provided, then it's penultimate
var expectedTrigger = args[args.Length - 1] is XmlCompletionTrigger
? args[args.Length - 1]
: args[args.Length - 2];
var caretMarkerIndex = doc.IndexOf ('|');
if (caretMarkerIndex > -1) {
doc = doc.Remove (caretMarkerIndex, 1);
}
var spanStartMarkerIndex = doc.IndexOf ('^');
if (spanStartMarkerIndex > -1) {
doc = doc.Remove (spanStartMarkerIndex, 1);
if (caretMarkerIndex > -1 && caretMarkerIndex >= spanStartMarkerIndex) {
caretMarkerIndex--;
}
}
var spanEndMarkerIndex = doc.IndexOf ('$');
if (spanEndMarkerIndex > -1) {
doc = doc.Remove (spanEndMarkerIndex, 1);
if (caretMarkerIndex > -1 && caretMarkerIndex >= spanEndMarkerIndex) {
caretMarkerIndex--;
}
if (spanStartMarkerIndex > -1 && spanStartMarkerIndex >= spanEndMarkerIndex) {
spanStartMarkerIndex--;
}
}

//length is optional, but if provided it's always last
int expectedLength = args[args.Length - 1] as int? ?? 0;
var triggerPos = caretMarkerIndex > -1 ? caretMarkerIndex : doc.Length;

if (typedChar != '\0') {
doc += typedChar;
}
int expectedSpanStart = spanStartMarkerIndex > -1 ? spanStartMarkerIndex : (triggerReason == XmlTriggerReason.TypedChar)? triggerPos - 1 : triggerPos;

int expectedSpanEnd = spanEndMarkerIndex > -1 ? spanEndMarkerIndex : triggerPos;
int expectedSpanLength = expectedSpanEnd - expectedSpanStart;

char typedChar = triggerReason == XmlTriggerReason.TypedChar ? doc[triggerPos - 1] : '\0';

var spine = new XmlSpineParser (new XmlRootState ());
spine.Parse (doc);
for (int i = spine.Position; i < triggerPos; i++) {
spine.Push (doc [i]);
}

var result = XmlCompletionTriggering.GetTriggerAndSpan (spine, triggerReason, typedChar, new StringTextSource (doc));

var result = XmlCompletionTriggering.GetTrigger (spine, reason, typedChar);
Assert.AreEqual (expectedTrigger, result.kind);
Assert.AreEqual (expectedLength, result.length);
if (expectedTrigger != XmlCompletionTrigger.None) {
Assert.AreEqual (expectedSpanStart, result.spanStart);
Assert.AreEqual (expectedSpanLength, result.spanLength);
}
}
}
}
Loading