blob: cc066a921447540db15a053fbe2010dedb36f740 [file] [log] [blame]
/*
* 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 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.codeInsight.template.emmet.completion.EmmetAbbreviationCompletionProvider;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
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.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import com.intellij.util.Consumer;
import com.intellij.util.ProcessingContext;
import com.intellij.xml.XmlBundle;
import com.intellij.xml.XmlExtension;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Set;
import static com.intellij.patterns.PlatformPatterns.psiElement;
/**
* @author Dmitry Avdeev
*/
public class XmlCompletionContributor extends CompletionContributor {
public static final Key<Boolean> WORD_COMPLETION_COMPATIBLE = Key.create("WORD_COMPLETION_COMPATIBLE");
@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 EmmetAbbreviationCompletionProvider());
extend(CompletionType.BASIC,
psiElement().inside(XmlPatterns.xmlAttributeValue()),
new CompletionProvider<CompletionParameters>() {
@Override
protected void addCompletions(@NotNull CompletionParameters parameters,
ProcessingContext context,
@NotNull final CompletionResultSet result) {
final XmlAttributeValue attributeValue = PsiTreeUtil.getParentOfType(parameters.getPosition(), XmlAttributeValue.class, false);
if (attributeValue == null) {
// we are injected, only getContext() returns attribute value
return;
}
final Set<String> usedWords = new THashSet<String>();
final Ref<Boolean> addWordVariants = Ref.create(true);
result.runRemainingContributors(parameters, new Consumer<CompletionResult>() {
@Override
public void consume(CompletionResult 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);
}
}
});
}
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(@NotNull final CompletionParameters parameters, @NotNull 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(@NotNull 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);
if (shortcut != null) {
return XmlBundle.message("tag.name.completion.hint", shortcut);
}
}
}
return super.advertise(parameters);
}
@Override
public void beforeCompletion(@NotNull 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());
}
}
}