com.intellij.codeInsight.completion.XmlCompletionContributor.java Source code

Java tutorial

Introduction

Here is the source code for com.intellij.codeInsight.completion.XmlCompletionContributor.java

Source

/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.codeInsight.completion;

import static com.intellij.patterns.PlatformPatterns.psiElement;
import static com.intellij.xml.util.XmlUtil.VALUE_ATTR_NAME;
import static com.intellij.xml.util.XmlUtil.findDescriptorFile;

import gnu.trove.THashSet;

import java.util.List;
import java.util.Set;

import javax.annotation.Nonnull;

import org.jetbrains.annotations.NonNls;

import javax.annotation.Nullable;
import com.intellij.codeInsight.lookup.InsertHandlerDecorator;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.codeInsight.lookup.LookupElementDecorator;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.ide.highlighter.XHtmlFileType;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.lang.ASTNode;
import com.intellij.lang.html.HTMLLanguage;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.editor.CaretModel;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.patterns.XmlPatterns;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.impl.source.html.dtd.HtmlElementDescriptorImpl;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlDocument;
import com.intellij.psi.xml.XmlEntityDecl;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlProlog;
import com.intellij.psi.xml.XmlTag;
import com.intellij.psi.xml.XmlTokenType;
import com.intellij.util.ProcessingContext;
import com.intellij.xml.Html5SchemaProvider;
import com.intellij.xml.XmlBundle;
import com.intellij.xml.XmlExtension;
import com.intellij.xml.XmlNSDescriptor;
import com.intellij.xml.util.HtmlUtil;
import com.intellij.xml.util.XmlEnumeratedValueReference;
import com.intellij.xml.util.XmlUtil;
import consulo.codeInsight.completion.CompletionProvider;

/**
 * @author Dmitry Avdeev
 */
public class XmlCompletionContributor extends CompletionContributor {
    public static final Key<Boolean> WORD_COMPLETION_COMPATIBLE = Key.create("WORD_COMPLETION_COMPATIBLE");
    public static final EntityRefInsertHandler ENTITY_INSERT_HANDLER = new EntityRefInsertHandler();

    @NonNls
    public static final String TAG_NAME_COMPLETION_FEATURE = "tag.name.completion";
    private static final InsertHandlerDecorator<LookupElement> QUOTE_EATER = new InsertHandlerDecorator<LookupElement>() {
        @Override
        public void handleInsert(InsertionContext context, LookupElementDecorator<LookupElement> item) {
            final char completionChar = context.getCompletionChar();
            if (completionChar == '\'' || completionChar == '\"') {
                context.setAddCompletionChar(false);
                item.getDelegate().handleInsert(context);

                final Editor editor = context.getEditor();
                final Document document = editor.getDocument();
                int tailOffset = editor.getCaretModel().getOffset();
                if (document.getTextLength() > tailOffset) {
                    final char c = document.getCharsSequence().charAt(tailOffset);
                    if (c == completionChar || completionChar == '\'') {
                        editor.getCaretModel().moveToOffset(tailOffset + 1);
                    }
                }
            } else {
                item.getDelegate().handleInsert(context);
            }
        }
    };

    public XmlCompletionContributor() {
        extend(CompletionType.BASIC, psiElement().inside(XmlPatterns.xmlFile()), new CompletionProvider() {
            @Override
            public void addCompletions(@Nonnull CompletionParameters parameters, ProcessingContext context,
                    @Nonnull CompletionResultSet result) {
                PsiElement position = parameters.getPosition();
                IElementType type = position.getNode().getElementType();
                if (type != XmlTokenType.XML_DATA_CHARACTERS && type != XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN) {
                    return;
                }
                if ((position.getPrevSibling() != null && position.getPrevSibling().textMatches("&"))
                        || position.textContains('&')) {
                    PrefixMatcher matcher = result.getPrefixMatcher();
                    String prefix = matcher.getPrefix();
                    if (prefix.startsWith("&")) {
                        prefix = prefix.substring(1);
                    } else if (prefix.contains("&")) {
                        prefix = prefix.substring(prefix.indexOf("&") + 1);
                    }

                    addEntityRefCompletions(position, result.withPrefixMatcher(prefix));
                }
            }
        });
        extend(CompletionType.BASIC, psiElement().inside(XmlPatterns.xmlAttributeValue()),
                new CompletionProvider() {
                    @Override
                    public void addCompletions(@Nonnull CompletionParameters parameters, ProcessingContext context,
                            @Nonnull final CompletionResultSet result) {
                        final PsiElement position = parameters.getPosition();
                        if (!position.getLanguage().isKindOf(XMLLanguage.INSTANCE)) {
                            return;
                        }
                        final XmlAttributeValue attributeValue = PsiTreeUtil.getParentOfType(position,
                                XmlAttributeValue.class, false);
                        if (attributeValue == null) {
                            // we are injected, only getContext() returns attribute value
                            return;
                        }

                        final Set<String> usedWords = new THashSet<>();
                        final Ref<Boolean> addWordVariants = Ref.create(true);
                        result.runRemainingContributors(parameters, r -> {
                            if (r.getLookupElement().getUserData(WORD_COMPLETION_COMPATIBLE) == null) {
                                addWordVariants.set(false);
                            }
                            usedWords.add(r.getLookupElement().getLookupString());
                            result.passResult(r.withLookupElement(
                                    LookupElementDecorator.withInsertHandler(r.getLookupElement(), QUOTE_EATER)));
                        });
                        if (addWordVariants.get().booleanValue()) {
                            addWordVariants.set(attributeValue.getReferences().length == 0);
                        }

                        if (addWordVariants.get().booleanValue() && parameters.getInvocationCount() > 0) {
                            WordCompletionContributor.addWordCompletionVariants(result, parameters, usedWords);
                        }
                    }
                });
        extend(CompletionType.BASIC, psiElement().withElementType(XmlTokenType.XML_DATA_CHARACTERS),
                new CompletionProvider() {
                    @Override
                    public void addCompletions(@Nonnull CompletionParameters parameters, ProcessingContext context,
                            @Nonnull CompletionResultSet result) {
                        XmlTag tag = PsiTreeUtil.getParentOfType(parameters.getPosition(), XmlTag.class, false);
                        if (tag != null && !hasEnumerationReference(parameters, result)) {
                            final XmlTag simpleContent = XmlUtil.getSchemaSimpleContent(tag);
                            if (simpleContent != null) {
                                XmlUtil.processEnumerationValues(simpleContent, (element) -> {
                                    String value = element.getAttributeValue(VALUE_ATTR_NAME);
                                    assert value != null;
                                    result.addElement(LookupElementBuilder.create(value));
                                    return true;
                                });
                            }
                        }
                    }
                });
    }

    static boolean hasEnumerationReference(CompletionParameters parameters, CompletionResultSet result) {
        Ref<Boolean> hasRef = Ref.create(false);
        LegacyCompletionContributor.processReferences(parameters, result, (reference, resultSet) -> {
            if (reference instanceof XmlEnumeratedValueReference) {
                hasRef.set(true);
            }
        });
        return hasRef.get();
    }

    public static boolean isXmlNameCompletion(final CompletionParameters parameters) {
        final ASTNode node = parameters.getPosition().getNode();
        return node != null && node.getElementType() == XmlTokenType.XML_NAME;
    }

    @Override
    public void fillCompletionVariants(@Nonnull final CompletionParameters parameters,
            @Nonnull final CompletionResultSet result) {
        super.fillCompletionVariants(parameters, result);
        if (result.isStopped()) {
            return;
        }

        final PsiElement element = parameters.getPosition();

        if (parameters.isExtendedCompletion()) {
            completeTagName(parameters, result);
        }

        else if (parameters.getCompletionType() == CompletionType.SMART) {
            new XmlSmartCompletionProvider().complete(parameters, result, element);
        }
    }

    static void completeTagName(CompletionParameters parameters, CompletionResultSet result) {
        PsiElement element = parameters.getPosition();
        if (!isXmlNameCompletion(parameters)) {
            return;
        }
        result.stopHere();
        PsiElement parent = element.getParent();
        if (!(parent instanceof XmlTag) || !(parameters.getOriginalFile() instanceof XmlFile)) {
            return;
        }
        final XmlTag tag = (XmlTag) parent;
        final String namespace = tag.getNamespace();
        final String prefix = result.getPrefixMatcher().getPrefix();
        final int pos = prefix.indexOf(':');

        final PsiReference reference = tag.getReference();
        String namespacePrefix = tag.getNamespacePrefix();

        if (reference != null && !namespace.isEmpty() && !namespacePrefix.isEmpty()) {
            // fallback to simple completion
            result.runRemainingContributors(parameters, true);
        } else {

            final CompletionResultSet newResult = result
                    .withPrefixMatcher(pos >= 0 ? prefix.substring(pos + 1) : prefix);

            final XmlFile file = (XmlFile) parameters.getOriginalFile();
            final List<XmlExtension.TagInfo> names = XmlExtension.getExtension(file).getAvailableTagNames(file,
                    tag);
            for (XmlExtension.TagInfo info : names) {
                final LookupElement item = createLookupElement(info, info.namespace,
                        namespacePrefix.isEmpty() ? null : namespacePrefix);
                newResult.addElement(item);
            }
        }
    }

    public static LookupElement createLookupElement(XmlExtension.TagInfo tagInfo, final String tailText,
            @Nullable String namespacePrefix) {
        LookupElementBuilder builder = LookupElementBuilder.create(tagInfo, tagInfo.name)
                .withInsertHandler(new ExtendedTagInsertHandler(tagInfo.name, tagInfo.namespace, namespacePrefix));
        if (!StringUtil.isEmpty(tailText)) {
            builder = builder.withTypeText(tailText, true);
        }
        return builder;
    }

    @Override
    public String advertise(@Nonnull final CompletionParameters parameters) {
        if (isXmlNameCompletion(parameters) && parameters.getCompletionType() == CompletionType.BASIC) {
            if (FeatureUsageTracker.getInstance().isToBeAdvertisedInLookup(TAG_NAME_COMPLETION_FEATURE,
                    parameters.getPosition().getProject())) {
                final String shortcut = getActionShortcut(IdeActions.ACTION_CODE_COMPLETION);
                return XmlBundle.message("tag.name.completion.hint", shortcut);
            }
        }
        return super.advertise(parameters);
    }

    @Override
    public void beforeCompletion(@Nonnull final CompletionInitializationContext context) {
        final int offset = context.getStartOffset();
        final PsiFile file = context.getFile();
        final XmlAttributeValue attributeValue = PsiTreeUtil.findElementOfClassAtOffset(file, offset,
                XmlAttributeValue.class, true);
        if (attributeValue != null && offset == attributeValue.getTextRange().getStartOffset()) {
            context.setDummyIdentifier("");
        }

        final PsiElement at = file.findElementAt(offset);
        if (at != null && at.getNode().getElementType() == XmlTokenType.XML_NAME
                && at.getParent() instanceof XmlAttribute) {
            context.getOffsetMap().addOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET,
                    at.getTextRange().getEndOffset());
        }
        if (at != null && at.getParent() instanceof XmlAttributeValue) {
            final int end = at.getParent().getTextRange().getEndOffset();
            final Document document = context.getEditor().getDocument();
            final int lineEnd = document.getLineEndOffset(document.getLineNumber(offset));
            if (lineEnd < end) {
                context.setReplacementOffset(lineEnd);
            }
        }
    }

    private static void addEntityRefCompletions(PsiElement context, CompletionResultSet resultSet) {
        XmlFile containingFile = null;
        XmlFile descriptorFile = null;
        final XmlTag tag = PsiTreeUtil.getParentOfType(context, XmlTag.class);

        if (tag != null) {
            containingFile = (XmlFile) tag.getContainingFile();
            descriptorFile = findDescriptorFile(tag, containingFile);
        }

        if (HtmlUtil.isHtml5Context(tag)) {
            descriptorFile = XmlUtil.findXmlFile(containingFile, Html5SchemaProvider.getCharsDtdLocation());
        } else if (tag == null) {
            final XmlDocument document = PsiTreeUtil.getParentOfType(context, XmlDocument.class);

            if (document != null) {
                containingFile = (XmlFile) document.getContainingFile();

                final FileType ft = containingFile.getFileType();
                if (HtmlUtil.isHtml5Document(document)) {
                    descriptorFile = XmlUtil.findXmlFile(containingFile, Html5SchemaProvider.getCharsDtdLocation());
                } else if (ft != XmlFileType.INSTANCE) {
                    final String namespace = ft == XHtmlFileType.INSTANCE ? XmlUtil.XHTML_URI : XmlUtil.HTML_URI;
                    final XmlNSDescriptor nsDescriptor = document.getDefaultNSDescriptor(namespace, true);

                    if (nsDescriptor != null) {
                        descriptorFile = nsDescriptor.getDescriptorFile();
                    }
                }
            }
        }

        if (descriptorFile != null && containingFile != null) {
            final boolean acceptSystemEntities = containingFile.getFileType() == XmlFileType.INSTANCE;
            final boolean caseInsensitive = (tag != null
                    && tag.getDescriptor() instanceof HtmlElementDescriptorImpl)
                    || containingFile.getLanguage().isKindOf(HTMLLanguage.INSTANCE);

            final PsiElementProcessor processor = new PsiElementProcessor() {
                @Override
                public boolean execute(@Nonnull final PsiElement element) {
                    if (element instanceof XmlEntityDecl) {
                        final XmlEntityDecl xmlEntityDecl = (XmlEntityDecl) element;
                        if (xmlEntityDecl.isInternalReference() || acceptSystemEntities) {
                            final LookupElementBuilder _item = buildEntityLookupItem(xmlEntityDecl);
                            if (_item != null) {
                                resultSet.addElement(_item.withCaseSensitivity(!caseInsensitive));
                                resultSet.stopHere();
                            }
                        }
                    }
                    return true;
                }
            };

            XmlUtil.processXmlElements(descriptorFile, processor, true);
            if (descriptorFile != containingFile && acceptSystemEntities) {
                final XmlProlog element = containingFile.getDocument().getProlog();
                if (element != null) {
                    XmlUtil.processXmlElements(element, processor, true);
                }
            }
        }
    }

    @Nullable
    private static LookupElementBuilder buildEntityLookupItem(@Nonnull final XmlEntityDecl decl) {
        final String name = decl.getName();
        if (name == null) {
            return null;
        }

        final LookupElementBuilder result = LookupElementBuilder.create(name)
                .withInsertHandler(ENTITY_INSERT_HANDLER);
        final XmlAttributeValue value = decl.getValueElement();
        final ASTNode node = value.getNode();
        if (node != null) {
            final ASTNode[] nodes = node.getChildren(TokenSet.create(XmlTokenType.XML_CHAR_ENTITY_REF));
            if (nodes.length == 1) {
                final String valueText = nodes[0].getText();
                final int i = valueText.indexOf('#');
                if (i > 0) {
                    String s = valueText.substring(i + 1);
                    s = StringUtil.trimEnd(s, ";");

                    try {
                        final int unicodeChar = Integer.valueOf(s).intValue();
                        return result.withTypeText(String.valueOf((char) unicodeChar));
                    } catch (NumberFormatException e) {
                        return result;
                    }
                }
            }
        }

        return result;
    }

    private static class EntityRefInsertHandler extends BasicInsertHandler<LookupElement> {
        @Override
        public void handleInsert(InsertionContext context, LookupElement item) {
            super.handleInsert(context, item);
            context.setAddCompletionChar(false);
            final CaretModel caretModel = context.getEditor().getCaretModel();
            context.getEditor().getDocument().insertString(caretModel.getOffset(), ";");
            caretModel.moveToOffset(caretModel.getOffset() + 1);
        }
    }
}