Skip to content

Commit

Permalink
Merge pull request #65 from mhutch/improve-triggering
Browse files Browse the repository at this point in the history
Improve triggering
  • Loading branch information
mhutch committed Jul 7, 2023
2 parents 5e38c03 + d541340 commit d62e1cc
Show file tree
Hide file tree
Showing 9 changed files with 394 additions and 211 deletions.
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

0 comments on commit d62e1cc

Please sign in to comment.