//FIXME: the length should do a readahead to capture the whole token
        public static (XmlCompletionTrigger kind, int length) GetTrigger(XmlSpineParser parser, XmlTriggerReason reason, char typedCharacter)
        {
            bool isExplicit  = reason == XmlTriggerReason.Invocation;
            bool isTypedChar = reason == XmlTriggerReason.TypedChar;
            bool isBackspace = reason == XmlTriggerReason.Backspace;

            if (isTypedChar)
            {
                Debug.Assert(typedCharacter != '\0');
            }

            var context = parser.GetContext();

            // explicit invocation in element name
            if (isExplicit && context.CurrentState is XmlNameState && context.CurrentState.Parent is XmlTagState)
            {
                int length = context.CurrentStateLength;
                return(XmlCompletionTrigger.Element, length);
            }

            //auto trigger after < in free space
            if ((isTypedChar || isBackspace) && XmlRootState.MaybeTag(context))
            {
                return(XmlCompletionTrigger.Element, 0);
            }

            //auto trigger after typing first char after < or fist char of attribute
            if (isTypedChar && context.CurrentStateLength == 1 && context.CurrentState is XmlNameState && XmlChar.IsFirstNameChar(typedCharacter))
            {
                if (context.CurrentState.Parent is XmlTagState)
                {
                    return(XmlCompletionTrigger.Element, 1);
                }
                if (context.CurrentState.Parent is XmlAttributeState)
                {
                    return(XmlCompletionTrigger.Attribute, 1);
                }
            }

            // trigger on explicit invocation after <
            if (isExplicit && XmlRootState.MaybeTag(context))
            {
                return(XmlCompletionTrigger.Element, 0);
            }

            //doctype/cdata completion, explicit trigger after <! or type ! after <
            if ((isExplicit || typedCharacter == '!') && XmlRootState.MaybeCDataOrCommentOrDocType(context))
            {
                return(XmlCompletionTrigger.DeclarationOrCDataOrComment, 2);
            }

            //explicit trigger in existing doctype
            if (isExplicit && (XmlRootState.MaybeDocType(context) || context.Nodes.Peek() is XDocType))
            {
                int length = context.CurrentState is XmlRootState ? context.CurrentStateLength : context.Position - ((XDocType)context.Nodes.Peek()).Span.Start;
                return(XmlCompletionTrigger.DocType, length);
            }

            //explicit trigger in attribute name
            if (isExplicit && context.CurrentState is XmlNameState && context.CurrentState.Parent is XmlAttributeState)
            {
                return(XmlCompletionTrigger.Attribute, context.CurrentStateLength);
            }

            //typed space or explicit trigger in tag
            if ((isExplicit || typedCharacter == ' ') && XmlTagState.IsFree(context))
            {
                return(XmlCompletionTrigger.Attribute, 0);
            }

            //attribute value completion
            if (XmlAttributeValueState.GetDelimiterChar(context).HasValue)
            {
                //auto trigger on quote regardless
                if (context.CurrentStateLength == 1)
                {
                    return(XmlCompletionTrigger.AttributeValue, 0);
                }
                if (isExplicit)
                {
                    return(XmlCompletionTrigger.AttributeValue, context.CurrentStateLength - 1);
                }
            }

            //entity completion
            if (context.CurrentState is XmlTextState || context.CurrentState is XmlAttributeValueState)
            {
                if (typedCharacter == '&')
                {
                    return(XmlCompletionTrigger.Entity, 0);
                }

                var text = parser.GetContext().KeywordBuilder;

                if (isBackspace && text[text.Length - 1] == '&')
                {
                    return(XmlCompletionTrigger.Entity, 0);
                }

                if (isExplicit)
                {
                    for (int i = 0; i < text.Length; i++)
                    {
                        var c = text[text.Length - i - 1];
                        if (c == '&')
                        {
                            return(XmlCompletionTrigger.Entity, i);
                        }
                        if (!XmlChar.IsNameChar(c))
                        {
                            break;
                        }
                    }
                }
            }

            //explicit invocation in free space
            if (isExplicit && (
                    context.CurrentState is XmlTextState ||
                    XmlRootState.IsFree(context)
                    ))
            {
                return(XmlCompletionTrigger.ElementWithBracket, 0);
            }

            return(XmlCompletionTrigger.None, 0);
        }
        //FIXME: the length should do a readahead to capture the whole token
        public static (XmlCompletionTrigger kind, int length) GetTrigger(XmlParser spine, XmlTriggerReason reason, char typedCharacter)
        {
            int  stateTag    = ((IXmlParserContext)spine).StateTag;
            bool isExplicit  = reason == XmlTriggerReason.Invocation;
            bool isTypedChar = reason == XmlTriggerReason.TypedChar;
            bool isBackspace = reason == XmlTriggerReason.Backspace;

            Debug.Assert(!isTypedChar || typedCharacter == '\0');

            // explicit invocation in element name
            if (isExplicit && spine.CurrentState is XmlNameState && spine.Nodes.Peek() is XElement el && !el.IsNamed)
            {
                int length = spine.CurrentStateLength;
                return(XmlCompletionTrigger.Element, length);
            }

            //auto trigger after < in free space
            if (spine.CurrentState is XmlRootState && stateTag == XmlRootState.BRACKET)
            {
                return(XmlCompletionTrigger.Element, 0);
            }

            // trigger on explicit invocation after <
            if (isExplicit && spine.CurrentState is XmlRootState && stateTag == XmlRootState.BRACKET)
            {
                return(XmlCompletionTrigger.Element, 0);
            }

            //doctype/cdata completion, explicit trigger after <! or type ! after <
            if ((isExplicit || typedCharacter == '!') && spine.CurrentState is XmlRootState && stateTag == XmlRootState.BRACKET_EXCLAM)
            {
                return(XmlCompletionTrigger.DeclarationOrCDataOrComment, 2);
            }

            //explicit trigger in existing declaration
            if (isExplicit && ((spine.CurrentState is XmlRootState && stateTag == XmlRootState.DOCTYPE) || spine.Nodes.Peek() is XDocType))
            {
                int length = spine.CurrentState is XmlRootState ? spine.CurrentStateLength : spine.Position - ((XDocType)spine.Nodes.Peek()).Span.Start;
                return(XmlCompletionTrigger.DocType, length);
            }

            //explicit trigger in attribute name
            if (isExplicit && spine.CurrentState is XmlNameState && spine.CurrentState.Parent is XmlAttributeState)
            {
                return(XmlCompletionTrigger.Attribute, spine.CurrentStateLength);
            }

            //typed space or explicit trigger in tag
            if ((isExplicit || typedCharacter == ' ') && spine.CurrentState is XmlTagState && stateTag == XmlTagState.FREE)
            {
                return(XmlCompletionTrigger.Attribute, 0);
            }

            //attribute value completion
            if (spine.CurrentState is XmlAttributeValueState)
            {
                var kind = stateTag & XmlAttributeValueState.TagMask;
                if (kind == XmlAttributeValueState.DOUBLEQUOTE || kind == XmlAttributeValueState.SINGLEQUOTE)
                {
                    //auto trigger on quote regardless
                    if (spine.CurrentStateLength == 1)
                    {
                        return(XmlCompletionTrigger.AttributeValue, 0);
                    }
                    if (isExplicit)
                    {
                        return(XmlCompletionTrigger.AttributeValue, spine.CurrentStateLength - 1);
                    }
                }
            }

            //entity completion
            if (spine.CurrentState is XmlTextState || spine.CurrentState is XmlAttributeValueState)
            {
                if (typedCharacter == '&')
                {
                    return(XmlCompletionTrigger.Entity, 0);
                }

                var text = ((IXmlParserContext)spine).KeywordBuilder;

                if (isBackspace && text[text.Length - 1] == '&')
                {
                    return(XmlCompletionTrigger.Entity, 0);
                }

                if (isExplicit)
                {
                    for (int i = 0; i < text.Length; i++)
                    {
                        var c = text[text.Length - i - 1];
                        if (c == '&')
                        {
                            return(XmlCompletionTrigger.Entity, i);
                        }
                        if (!XmlChar.IsNameChar(c))
                        {
                            break;
                        }
                    }
                }
            }

            //explicit invocation in free space
            if (isExplicit && (
                    spine.CurrentState is XmlTextState ||
                    (spine.CurrentState is XmlRootState && stateTag == XmlRootState.FREE)
                    ))
            {
                return(XmlCompletionTrigger.ElementWithBracket, 0);
            }

            return(XmlCompletionTrigger.None, 0);
        }