| /* |
| * Copyright 2000-2013 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.xml.util; |
| |
| import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider; |
| import com.intellij.codeInsight.lookup.LookupElement; |
| import com.intellij.codeInsight.lookup.LookupElementBuilder; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.ElementManipulators; |
| import com.intellij.psi.PsiElement; |
| import com.intellij.psi.PsiFile; |
| import com.intellij.psi.PsiReference; |
| import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference; |
| import com.intellij.psi.search.PsiElementProcessor; |
| import com.intellij.psi.util.CachedValue; |
| import com.intellij.psi.util.CachedValueProvider; |
| import com.intellij.psi.util.CachedValuesManager; |
| import com.intellij.psi.xml.*; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.containers.HashMap; |
| import com.intellij.xml.XmlBundle; |
| import com.intellij.xml.XmlExtension; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.Map; |
| |
| /** |
| * @author Maxim.Mossienko |
| */ |
| public class AnchorReferenceImpl implements AnchorReference, PsiReference, EmptyResolveMessageProvider { |
| private final String myAnchor; |
| private final FileReference myFileReference; |
| private final PsiElement myElement; |
| private final int myOffset; |
| private final boolean mySoft; |
| @NonNls |
| private static final String ANCHOR_ELEMENT_NAME = "a"; |
| @NonNls private static final String MAP_ELEMENT_NAME = "map"; |
| private static final Key<CachedValue<Map<String,XmlTag>>> ourCachedIdsKey = Key.create("cached.ids"); |
| |
| AnchorReferenceImpl(final String anchor, @Nullable final FileReference psiReference, final PsiElement element, final int offset, |
| final boolean soft) { |
| |
| myAnchor = anchor; |
| myFileReference = psiReference; |
| myElement = element; |
| myOffset = offset; |
| mySoft = soft; |
| } |
| |
| @Override |
| public PsiElement getElement() { |
| return myElement; |
| } |
| |
| @Override |
| public TextRange getRangeInElement() { |
| return new TextRange(myOffset,myOffset+myAnchor.length()); |
| } |
| |
| @Override |
| public PsiElement resolve() { |
| if (myAnchor.isEmpty()) { |
| return myElement; |
| } |
| Map<String,XmlTag> map = getIdMap(); |
| final XmlTag tag = map != null ? map.get(myAnchor):null; |
| if (tag != null) { |
| XmlAttribute attribute = tag.getAttribute("id"); |
| if (attribute==null) attribute = tag.getAttribute("name"); |
| |
| if (attribute == null && MAP_ELEMENT_NAME.equalsIgnoreCase(tag.getName())) { |
| attribute = tag.getAttribute("usemap"); |
| } |
| |
| assert attribute != null: tag.getText(); |
| return attribute.getValueElement(); |
| } |
| |
| return null; |
| } |
| |
| private static boolean processXmlElements(XmlTag element, PsiElementProcessor<XmlTag> processor) { |
| if (!_processXmlElements(element,processor)) return false; |
| |
| for(PsiElement next = element.getNextSibling(); next != null; next = next.getNextSibling()) { |
| if (next instanceof XmlTag) { |
| if (!_processXmlElements((XmlTag)next,processor)) return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| static boolean _processXmlElements(XmlTag element, PsiElementProcessor<XmlTag> processor) { |
| if (!processor.execute(element)) return false; |
| final XmlTag[] subTags = element.getSubTags(); |
| |
| for (XmlTag subTag : subTags) { |
| if (!_processXmlElements(subTag, processor)) return false; |
| } |
| |
| return true; |
| } |
| |
| @Nullable |
| private Map<String,XmlTag> getIdMap() { |
| final XmlFile file = getFile(); |
| |
| if (file != null) { |
| CachedValue<Map<String, XmlTag>> value = file.getUserData(ourCachedIdsKey); |
| if (value == null) { |
| value = CachedValuesManager.getManager(file.getProject()).createCachedValue(new MapCachedValueProvider(file), false); |
| file.putUserData(ourCachedIdsKey, value); |
| } |
| |
| return value.getValue(); |
| } |
| return null; |
| } |
| |
| @Nullable |
| private static String getAnchorValue(final XmlTag xmlTag) { |
| final String attributeValue = xmlTag.getAttributeValue("id"); |
| |
| if (attributeValue!=null) { |
| return attributeValue; |
| } |
| |
| if (ANCHOR_ELEMENT_NAME.equalsIgnoreCase(xmlTag.getName())) { |
| final String attributeValue2 = xmlTag.getAttributeValue("name"); |
| if (attributeValue2!=null) { |
| return attributeValue2; |
| } |
| } |
| |
| if (MAP_ELEMENT_NAME.equalsIgnoreCase(xmlTag.getName())) { |
| final String map_anchor = xmlTag.getAttributeValue("name"); |
| if (map_anchor != null) { |
| return map_anchor; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Override |
| @NotNull |
| public String getCanonicalText() { |
| return myAnchor; |
| } |
| |
| @Override |
| public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException { |
| return ElementManipulators.getManipulator(myElement).handleContentChange( |
| myElement, |
| getRangeInElement(), |
| newElementName |
| ); |
| } |
| |
| @Override |
| @Nullable |
| public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException { |
| return null; |
| } |
| |
| @Override |
| public boolean isReferenceTo(PsiElement element) { |
| return element instanceof XmlAttributeValue && myElement.getManager().areElementsEquivalent(element, resolve()); |
| } |
| |
| @Override |
| @NotNull |
| public Object[] getVariants() { |
| final Map<String, XmlTag> idMap = getIdMap(); |
| if (idMap == null) return ArrayUtil.EMPTY_OBJECT_ARRAY; |
| |
| String[] variants = idMap.keySet().toArray(new String[idMap.size()]); |
| LookupElement[] elements = new LookupElement[variants.length]; |
| for (int i = 0, variantsLength = variants.length; i < variantsLength; i++) { |
| elements[i] = LookupElementBuilder.create(variants[i]).withCaseSensitivity(true); |
| } |
| return elements; |
| } |
| |
| @Nullable |
| private XmlFile getFile() { |
| if (myFileReference != null) { |
| final PsiElement psiElement = myFileReference.resolve(); |
| return psiElement instanceof XmlFile ? (XmlFile)psiElement:null; |
| } |
| |
| final PsiFile containingFile = myElement.getContainingFile(); |
| if (containingFile instanceof XmlFile) { |
| return (XmlFile)containingFile; |
| } |
| else { |
| final XmlExtension extension = XmlExtension.getExtensionByElement(myElement); |
| return extension == null ? null : extension.getContainingFile(myElement); |
| } |
| } |
| |
| @Override |
| public boolean isSoft() { |
| return mySoft; |
| } |
| |
| @Override |
| @NotNull |
| public String getUnresolvedMessagePattern() { |
| final XmlFile xmlFile = getFile(); |
| return xmlFile == null ? |
| XmlBundle.message("cannot.resolve.anchor", myAnchor) : |
| XmlBundle.message("cannot.resolve.anchor.in.file", myAnchor, xmlFile.getName()); |
| } |
| |
| // separate static class to avoid memory leak via this$0 |
| private static class MapCachedValueProvider implements CachedValueProvider<Map<String, XmlTag>> { |
| private final XmlFile myFile; |
| |
| public MapCachedValueProvider(XmlFile file) { |
| myFile = file; |
| } |
| |
| @Override |
| public Result<Map<String, XmlTag>> compute() { |
| final Map<String,XmlTag> resultMap = new HashMap<String, XmlTag>(); |
| XmlDocument document = HtmlUtil.getRealXmlDocument(myFile.getDocument()); |
| final XmlTag rootTag = document != null ? document.getRootTag():null; |
| |
| if (rootTag != null) { |
| processXmlElements(rootTag, |
| new PsiElementProcessor<XmlTag>() { |
| @Override |
| public boolean execute(@NotNull final XmlTag element) { |
| final String anchorValue = getAnchorValue(element); |
| |
| if (anchorValue!=null) { |
| resultMap.put(anchorValue, element); |
| } |
| return true; |
| } |
| } |
| ); |
| } |
| return new Result<Map<String, XmlTag>>(resultMap, myFile); |
| } |
| } |
| } |