protected virtual async Task <ICompletionDataList> HandleCodeCompletion(
            CodeCompletionContext completionContext, bool forced, CancellationToken token)
        {
            var buf = this.Editor;

            // completionChar may be a space even if the current char isn't, when ctrl-space is fired t
            var  currentLocation = new DocumentLocation(completionContext.TriggerLine, completionContext.TriggerLineOffset);
            char currentChar     = completionContext.TriggerOffset < 1? ' ' : buf.GetCharAt(completionContext.TriggerOffset - 1);
            char previousChar    = completionContext.TriggerOffset < 2? ' ' : buf.GetCharAt(completionContext.TriggerOffset - 2);

            LoggingService.LogDebug("Attempting completion for state '{0}'x{1}, previousChar='{2}',"
                                    + " currentChar='{3}', forced='{4}'", tracker.Engine.CurrentState,
                                    tracker.Engine.CurrentStateLength, previousChar, currentChar, forced);

            //closing tag completion
            if (tracker.Engine.CurrentState is XmlRootState && currentChar == '>')
            {
                return(ClosingTagCompletion(buf, currentLocation));
            }

            // Auto insert '>' when '/' is typed inside tag state (for quick tag closing)
            //FIXME: avoid doing this when the next non-whitespace char is ">" or ignore the next ">" typed
            if (XmlEditorOptions.AutoInsertFragments && tracker.Engine.CurrentState is XmlTagState && currentChar == '/')
            {
                buf.InsertAtCaret(">");
                return(null);
            }

            //entity completion
            if (currentChar == '&' && (tracker.Engine.CurrentState is XmlRootState ||
                                       tracker.Engine.CurrentState is XmlAttributeValueState))
            {
                var list = new CompletionDataList();

                //TODO: need to tweak semicolon insertion
                list.Add(new BaseXmlCompletionData("apos", "'"));
                list.Add(new BaseXmlCompletionData("quot", "\""));
                list.Add(new BaseXmlCompletionData("lt", "<"));
                list.Add(new BaseXmlCompletionData("gt", ">"));
                list.Add(new BaseXmlCompletionData("amp", "&"));

                //not sure about these "completions". they're more like
                //shortcuts than completions but they're pretty useful
                list.Add(new BaseXmlCompletionData("'")
                {
                    CompletionText = "apos;"
                });
                list.Add(new BaseXmlCompletionData("\"")
                {
                    CompletionText = "quot;"
                });
                list.Add(new BaseXmlCompletionData("<")
                {
                    CompletionText = "lt;"
                });
                list.Add(new BaseXmlCompletionData(">")
                {
                    CompletionText = "gt;"
                });
                list.Add(new BaseXmlCompletionData("&")
                {
                    CompletionText = "amp;"
                });

                var ecList = await GetEntityCompletions(token);

                list.AddRange(ecList);
                return(list);
            }

            //doctype completion
            if (tracker.Engine.CurrentState is XmlDocTypeState)
            {
                if (tracker.Engine.CurrentStateLength == 1)
                {
                    CompletionDataList list = await GetDocTypeCompletions(token);

                    if (list != null && list.Count > 0)
                    {
                        return(list);
                    }
                }
                return(null);
            }

            //attribute value completion
            //determine whether to trigger completion within attribute values quotes
            if ((Tracker.Engine.CurrentState is XmlAttributeValueState)
                //trigger on the opening quote
                && ((Tracker.Engine.CurrentStateLength == 1 && (currentChar == '\'' || currentChar == '"'))
                    //or trigger on first letter of value, if unforced
                    || (forced || Tracker.Engine.CurrentStateLength == 2)))
            {
                var att = (XAttribute)Tracker.Engine.Nodes.Peek();

                if (att.IsNamed)
                {
                    var attributedOb = Tracker.Engine.Nodes.Peek(1) as IAttributedXObject;
                    if (attributedOb == null)
                    {
                        return(null);
                    }

                    //if triggered by first letter of value or forced, grab those letters

                    var result = await GetAttributeValueCompletions(attributedOb, att, token);

                    if (result != null)
                    {
                        result.TriggerWordLength = Tracker.Engine.CurrentStateLength - 1;
                        return(result);
                    }
                    return(null);
                }
            }

            //attribute name completion
            if ((forced && Tracker.Engine.Nodes.Peek() is IAttributedXObject && !tracker.Engine.Nodes.Peek().IsEnded) ||
                ((Tracker.Engine.CurrentState is XmlNameState &&
                  Tracker.Engine.CurrentState.Parent is XmlAttributeState) ||
                 Tracker.Engine.CurrentState is XmlTagState))
            {
                IAttributedXObject attributedOb = (Tracker.Engine.Nodes.Peek() as IAttributedXObject) ??
                                                  Tracker.Engine.Nodes.Peek(1) as IAttributedXObject;

                if (attributedOb == null || !attributedOb.Name.IsValid)
                {
                    return(null);
                }

                var currentIsNameStart   = XmlNameState.IsValidNameStart(currentChar);
                var currentIsWhiteSpace  = char.IsWhiteSpace(currentChar);
                var previousIsWhiteSpace = char.IsWhiteSpace(previousChar);

                bool shouldTriggerAttributeCompletion = forced ||
                                                        (currentIsNameStart && previousIsWhiteSpace) ||
                                                        currentIsWhiteSpace;
                if (!shouldTriggerAttributeCompletion)
                {
                    return(null);
                }

                var existingAtts = new Dictionary <string, string> (StringComparer.OrdinalIgnoreCase);

                foreach (XAttribute att in attributedOb.Attributes)
                {
                    existingAtts [att.Name.FullName] = att.Value ?? string.Empty;
                }

                var result = await GetAttributeCompletions(attributedOb, existingAtts, token);

                if (result != null)
                {
                    if (!forced && currentIsNameStart)
                    {
                        result.TriggerWordLength = 1;
                    }
                    result.AutoSelect = !currentIsWhiteSpace;
                    result.AddKeyHandler(new AttributeKeyHandler());
                    return(result);
                }
            }

            //element completion
            if (currentChar == '<' && tracker.Engine.CurrentState is XmlRootState ||
                (tracker.Engine.CurrentState is XmlNameState && forced))
            {
                var list = await GetElementCompletions(token);

                if (completionContext.TriggerLine == 1 && completionContext.TriggerOffset == 1)
                {
                    var encoding = Editor.Encoding.WebName;
                    list.Add(new BaseXmlCompletionData($"?xml version=\"1.0\" encoding=\"{encoding}\" ?>"));
                }
                AddCloseTag(list, Tracker.Engine.Nodes);
                return(list.Count > 0 ? list : null);
            }

            if (forced && Tracker.Engine.CurrentState is XmlRootState)
            {
                var list = new CompletionDataList();
                MonoDevelop.Ide.CodeTemplates.CodeTemplateService.AddCompletionDataForFileName(DocumentContext.Name, list);
                return(list.Count > 0? list : null);
            }

            return(null);
        }