| /* |
| * 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.psi.impl.source.resolve.reference.impl.providers; |
| |
| import com.intellij.codeInsight.daemon.EmptyResolveMessageProvider; |
| import com.intellij.codeInspection.LocalQuickFix; |
| import com.intellij.codeInspection.LocalQuickFixProvider; |
| import com.intellij.lang.LangBundle; |
| import com.intellij.lang.injection.InjectedLanguageManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.openapi.util.io.FileUtil; |
| import com.intellij.openapi.util.text.StringUtil; |
| import com.intellij.openapi.vfs.VfsUtilCore; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.openapi.vfs.VirtualFileSystem; |
| import com.intellij.psi.*; |
| import com.intellij.psi.impl.source.resolve.ResolveCache; |
| import com.intellij.psi.impl.source.resolve.reference.impl.CachingReference; |
| import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; |
| import com.intellij.psi.search.PsiFileSystemItemProcessor; |
| import com.intellij.refactoring.rename.BindablePsiReference; |
| import com.intellij.util.ArrayUtil; |
| import com.intellij.util.IncorrectOperationException; |
| import gnu.trove.THashSet; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.net.URI; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| |
| /** |
| * @author cdr |
| */ |
| public class FileReference implements PsiFileReference, FileReferenceOwner, PsiPolyVariantReference, |
| LocalQuickFixProvider, |
| EmptyResolveMessageProvider, BindablePsiReference { |
| |
| private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference"); |
| |
| public static final FileReference[] EMPTY = new FileReference[0]; |
| |
| private final int myIndex; |
| private TextRange myRange; |
| private final String myText; |
| @NotNull private final FileReferenceSet myFileReferenceSet; |
| |
| public FileReference(@NotNull final FileReferenceSet fileReferenceSet, TextRange range, int index, String text) { |
| myFileReferenceSet = fileReferenceSet; |
| myIndex = index; |
| myRange = range; |
| myText = text; |
| } |
| |
| public FileReference(final FileReference original) { |
| this(original.myFileReferenceSet, original.myRange, original.myIndex, original.myText); |
| } |
| |
| @Nullable |
| public static FileReference findFileReference(@NotNull final PsiReference original) { |
| if (original instanceof PsiMultiReference) { |
| final PsiMultiReference multiReference = (PsiMultiReference)original; |
| for (PsiReference reference : multiReference.getReferences()) { |
| if (reference instanceof FileReference) { |
| return (FileReference)reference; |
| } |
| } |
| } |
| else if (original instanceof FileReferenceOwner) { |
| final PsiFileReference fileReference = ((FileReferenceOwner)original).getLastFileReference(); |
| if (fileReference instanceof FileReference) { |
| return (FileReference)fileReference; |
| } |
| } |
| |
| return null; |
| } |
| |
| @NotNull |
| protected Collection<PsiFileSystemItem> getContexts() { |
| final FileReference contextRef = getContextReference(); |
| ArrayList<PsiFileSystemItem> result = new ArrayList<PsiFileSystemItem>(); |
| |
| if (contextRef == null) { |
| Collection<PsiFileSystemItem> defaultContexts = myFileReferenceSet.getDefaultContexts(); |
| for (PsiFileSystemItem context : defaultContexts) { |
| if (context == null) { |
| LOG.error(myFileReferenceSet.getClass() + " provided a null context"); |
| } |
| } |
| result.addAll(defaultContexts); |
| } |
| else { |
| ResolveResult[] resolveResults = contextRef.multiResolve(false); |
| for (ResolveResult resolveResult : resolveResults) { |
| if (resolveResult.getElement() != null) { |
| result.add((PsiFileSystemItem)resolveResult.getElement()); |
| } |
| } |
| } |
| |
| result.addAll(myFileReferenceSet.getExtraContexts()); |
| |
| return result; |
| } |
| |
| @Override |
| @NotNull |
| public ResolveResult[] multiResolve(final boolean incompleteCode) { |
| PsiFile file = getElement().getContainingFile(); |
| return ResolveCache.getInstance(file.getProject()).resolveWithCaching(this, MyResolver.INSTANCE, false, false, file); |
| } |
| |
| @NotNull |
| protected ResolveResult[] innerResolve(boolean caseSensitive, @NotNull PsiFile containingFile) { |
| final String referenceText = getText(); |
| if (referenceText.isEmpty() && myIndex == 0) { |
| return new ResolveResult[]{new PsiElementResolveResult(containingFile)}; |
| } |
| final Collection<PsiFileSystemItem> contexts = getContexts(); |
| final Collection<ResolveResult> result = new THashSet<ResolveResult>(); |
| for (final PsiFileSystemItem context : contexts) { |
| innerResolveInContext(referenceText, context, result, caseSensitive); |
| } |
| if (contexts.isEmpty() && isAllowedEmptyPath(referenceText)) { |
| result.add(new PsiElementResolveResult(containingFile)); |
| } |
| final int resultCount = result.size(); |
| return resultCount > 0 ? result.toArray(new ResolveResult[resultCount]) : ResolveResult.EMPTY_ARRAY; |
| } |
| |
| protected void innerResolveInContext(@NotNull final String text, |
| @NotNull PsiFileSystemItem context, |
| final Collection<ResolveResult> result, |
| final boolean caseSensitive) { |
| if (isAllowedEmptyPath(text) || ".".equals(text) || "/".equals(text)) { |
| result.add(new PsiElementResolveResult(context)); |
| } |
| else if ("..".equals(text)) { |
| final PsiFileSystemItem resolved = context.getParent(); |
| if (resolved != null) { |
| result.add(new PsiElementResolveResult(resolved)); |
| } |
| } |
| else { |
| final int separatorIndex = text.indexOf('/'); |
| if (separatorIndex >= 0) { |
| final List<ResolveResult> resolvedContexts = new ArrayList<ResolveResult>(); |
| if (separatorIndex == 0 /*starts with slash*/ && "/".equals(context.getName())) { |
| resolvedContexts.add(new PsiElementResolveResult(context)); |
| } |
| else { |
| innerResolveInContext(text.substring(0, separatorIndex), context, resolvedContexts, caseSensitive); |
| } |
| final String restOfText = text.substring(separatorIndex + 1); |
| for (ResolveResult contextVariant : resolvedContexts) { |
| final PsiFileSystemItem item = (PsiFileSystemItem)contextVariant.getElement(); |
| if (item != null) { |
| innerResolveInContext(restOfText, item, result, caseSensitive); |
| } |
| } |
| } |
| else { |
| final String decoded = decode(text); |
| |
| if (context instanceof PackagePrefixFileSystemItem) { |
| context = ((PackagePrefixFileSystemItem)context).getDirectory(); |
| } |
| else if (context instanceof FileReferenceResolver) { |
| PsiFileSystemItem child = ((FileReferenceResolver)context).resolveFileReference(this, decoded); |
| if (child != null) { |
| result.add(new PsiElementResolveResult(getOriginalFile(child))); |
| return; |
| } |
| } |
| |
| if (context.getParent() == null && FileUtil.namesEqual(decoded, context.getName())) { |
| // match filesystem roots |
| result.add(new PsiElementResolveResult(getOriginalFile(context))); |
| } |
| else if (context instanceof PsiDirectory && caseSensitivityApplies((PsiDirectory)context, caseSensitive)) { |
| // optimization: do not load all children into VFS |
| PsiDirectory directory = (PsiDirectory)context; |
| PsiFileSystemItem child = directory.findFile(decoded); |
| if (child == null) child = directory.findSubdirectory(decoded); |
| if (child != null) { |
| result.add(new PsiElementResolveResult(getOriginalFile(child))); |
| } |
| } |
| else { |
| processVariants(context, new PsiFileSystemItemProcessor() { |
| @Override |
| public boolean acceptItem(String name, boolean isDirectory) { |
| return caseSensitive ? decoded.equals(name) : decoded.compareToIgnoreCase(name) == 0; |
| } |
| |
| @Override |
| public boolean execute(@NotNull PsiFileSystemItem element) { |
| result.add(new PsiElementResolveResult(getOriginalFile(element))); |
| return true; |
| } |
| }); |
| } |
| } |
| } |
| } |
| |
| @NotNull |
| public String getFileNameToCreate() { |
| return decode(getCanonicalText()); |
| } |
| |
| @Nullable |
| public |
| String getNewFileTemplateName() { |
| return null; |
| } |
| |
| private static boolean caseSensitivityApplies(PsiDirectory context, boolean caseSensitive) { |
| VirtualFileSystem fs = context.getVirtualFile().getFileSystem(); |
| return fs.isCaseSensitive() == caseSensitive; |
| } |
| |
| private boolean isAllowedEmptyPath(String text) { |
| return text.isEmpty() && isLast() && |
| (StringUtil.isEmpty(myFileReferenceSet.getPathString()) && myFileReferenceSet.isEmptyPathAllowed() || |
| !myFileReferenceSet.isEndingSlashNotAllowed() && myIndex > 0); |
| } |
| |
| @NotNull |
| public String decode(@NotNull final String text) { |
| // strip http get parameters |
| String _text = text; |
| int paramIndex = text.lastIndexOf('?'); |
| if (paramIndex >= 0) { |
| _text = text.substring(0, paramIndex); |
| } |
| |
| if (myFileReferenceSet.isUrlEncoded()) { |
| try { |
| return StringUtil.notNullize(new URI(_text).getPath(), text); |
| } |
| catch (Exception ignored) { |
| return text; |
| } |
| } |
| |
| return _text; |
| } |
| |
| @Override |
| @NotNull |
| public Object[] getVariants() { |
| FileReferenceCompletion completion = FileReferenceCompletion.getInstance(); |
| if (completion != null) { |
| return completion.getFileReferenceCompletionVariants(this); |
| } |
| return ArrayUtil.EMPTY_OBJECT_ARRAY; |
| } |
| |
| /** |
| * Generates a lookup item for the specified completion variant candidate. |
| * |
| * @param candidate the element to show in the completion list. |
| * @return the lookup item representation (PsiElement, LookupElement or String). If returns null, |
| * {@code FileInfoManager.getFileLookupItem(candidate)} will be used to create the lookup item. |
| */ |
| protected Object createLookupItem(PsiElement candidate) { |
| return null; |
| } |
| |
| /** |
| * Converts a wrapper like WebDirectoryElement into plain PsiFile |
| */ |
| protected static PsiFileSystemItem getOriginalFile(PsiFileSystemItem fileSystemItem) { |
| final VirtualFile file = fileSystemItem.getVirtualFile(); |
| if (file != null && !file.isDirectory()) { |
| final PsiManager psiManager = fileSystemItem.getManager(); |
| if (psiManager != null) { |
| final PsiFile psiFile = psiManager.findFile(file); |
| if (psiFile != null) { |
| fileSystemItem = psiFile; |
| } |
| } |
| } |
| return fileSystemItem; |
| } |
| |
| @Nullable |
| protected String encode(final String name, PsiElement psiElement) { |
| try { |
| return new URI(null, null, name, null).toString(); |
| } |
| catch (Exception ignored) { |
| return name; |
| } |
| } |
| |
| protected static void processVariants(final PsiFileSystemItem context, final PsiFileSystemItemProcessor processor) { |
| context.processChildren(processor); |
| } |
| |
| @Nullable |
| private FileReference getContextReference() { |
| return myIndex > 0 ? myFileReferenceSet.getReference(myIndex - 1) : null; |
| } |
| |
| @Override |
| public PsiElement getElement() { |
| return myFileReferenceSet.getElement(); |
| } |
| |
| @Override |
| public PsiFileSystemItem resolve() { |
| ResolveResult[] resolveResults = multiResolve(false); |
| return resolveResults.length == 1 ? (PsiFileSystemItem)resolveResults[0].getElement() : null; |
| } |
| |
| @Nullable |
| public PsiFileSystemItem innerSingleResolve(final boolean caseSensitive, @NotNull PsiFile containingFile) { |
| final ResolveResult[] resolveResults = innerResolve(caseSensitive, containingFile); |
| return resolveResults.length == 1 ? (PsiFileSystemItem)resolveResults[0].getElement() : null; |
| } |
| |
| @Override |
| public boolean isReferenceTo(PsiElement element) { |
| if (!(element instanceof PsiFileSystemItem)) return false; |
| |
| final PsiFileSystemItem item = resolve(); |
| return item != null && FileReferenceHelperRegistrar.areElementsEquivalent(item, (PsiFileSystemItem)element); |
| } |
| |
| @Override |
| public TextRange getRangeInElement() { |
| return myRange; |
| } |
| |
| @Override |
| @NotNull |
| public String getCanonicalText() { |
| return myText; |
| } |
| |
| public String getText() { |
| return myText; |
| } |
| |
| @Override |
| public boolean isSoft() { |
| return myFileReferenceSet.isSoft(); |
| } |
| |
| @Override |
| public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException { |
| final ElementManipulator<PsiElement> manipulator = CachingReference.getManipulator(getElement()); |
| myFileReferenceSet.setElement(manipulator.handleContentChange(getElement(), getRangeInElement(), newElementName)); |
| //Correct ranges |
| int delta = newElementName.length() - myRange.getLength(); |
| myRange = new TextRange(getRangeInElement().getStartOffset(), getRangeInElement().getStartOffset() + newElementName.length()); |
| FileReference[] references = myFileReferenceSet.getAllReferences(); |
| for (int idx = myIndex + 1; idx < references.length; idx++) { |
| references[idx].myRange = references[idx].myRange.shiftRight(delta); |
| } |
| return myFileReferenceSet.getElement(); |
| } |
| |
| public PsiElement bindToElement(@NotNull final PsiElement element, final boolean absolute) throws IncorrectOperationException { |
| if (!(element instanceof PsiFileSystemItem)) { |
| throw new IncorrectOperationException("Cannot bind to element, should be instanceof PsiFileSystemItem: " + element); |
| } |
| |
| // handle empty reference that resolves to current file |
| if (getCanonicalText().isEmpty() && element == getElement().getContainingFile()) return getElement(); |
| |
| final PsiFileSystemItem fileSystemItem = (PsiFileSystemItem)element; |
| VirtualFile dstVFile = fileSystemItem.getVirtualFile(); |
| if (dstVFile == null) throw new IncorrectOperationException("Cannot bind to non-physical element:" + element); |
| |
| PsiFile file = getElement().getContainingFile(); |
| PsiElement contextPsiFile = InjectedLanguageManager.getInstance(file.getProject()).getInjectionHost(file); |
| if (contextPsiFile != null) file = contextPsiFile.getContainingFile(); // use host file! |
| final VirtualFile curVFile = file.getVirtualFile(); |
| if (curVFile == null) throw new IncorrectOperationException("Cannot bind from non-physical element:" + file); |
| |
| final Project project = element.getProject(); |
| |
| String newName; |
| |
| if (absolute) { |
| PsiFileSystemItem root = null; |
| PsiFileSystemItem dstItem = null; |
| for (final FileReferenceHelper helper : FileReferenceHelperRegistrar.getHelpers()) { |
| if (!helper.isMine(project, dstVFile)) continue; |
| PsiFileSystemItem _dstItem = helper.getPsiFileSystemItem(project, dstVFile); |
| if (_dstItem != null) { |
| PsiFileSystemItem _root = helper.findRoot(project, dstVFile); |
| if (_root != null) { |
| root = _root; |
| dstItem = _dstItem; |
| break; |
| } |
| } |
| } |
| if (root == null) { |
| PsiFileSystemItem _dstItem = NullFileReferenceHelper.INSTANCE.getPsiFileSystemItem(project, dstVFile); |
| if (_dstItem != null) { |
| PsiFileSystemItem _root = NullFileReferenceHelper.INSTANCE.findRoot(project, dstVFile); |
| if (_root != null) { |
| root = _root; |
| dstItem = _dstItem; |
| } |
| } |
| |
| if (root == null) { |
| return getElement(); |
| } |
| } |
| |
| final String relativePath = PsiFileSystemItemUtil.getRelativePath(root, dstItem); |
| if (relativePath == null) { |
| return getElement(); |
| } |
| newName = myFileReferenceSet.getNewAbsolutePath(root, relativePath); |
| } |
| else { // relative path |
| |
| final FileReferenceHelper helper = FileReferenceHelperRegistrar.getNotNullHelper(file); |
| |
| final Collection<PsiFileSystemItem> contexts = getContextsForBindToElement(curVFile, project, helper); |
| switch (contexts.size()) { |
| case 0: |
| break; |
| default: |
| for (PsiFileSystemItem context : contexts) { |
| final VirtualFile contextFile = context.getVirtualFile(); |
| assert contextFile != null; |
| if (VfsUtilCore.isAncestor(contextFile, dstVFile, true)) { |
| final String path = VfsUtilCore.getRelativePath(dstVFile, contextFile, '/'); |
| if (path != null) { |
| return rename(path); |
| } |
| } |
| } |
| } |
| |
| PsiFileSystemItem dstItem = helper.getPsiFileSystemItem(project, dstVFile); |
| PsiFileSystemItem curItem = helper.getPsiFileSystemItem(project, curVFile); |
| |
| if (curItem == null) { |
| throw new IncorrectOperationException("Cannot find path between files; " + |
| "src = " + curVFile.getPresentableUrl() + "; " + |
| "dst = " + dstVFile.getPresentableUrl() + "; " + |
| "Contexts: " + contexts); |
| } |
| if (curItem.equals(dstItem)) { |
| if (getCanonicalText().equals(dstItem.getName())) { |
| return getElement(); |
| } |
| return fixRefText(file.getName()); |
| } |
| newName = PsiFileSystemItemUtil.getRelativePath(curItem, dstItem); |
| if (newName == null) { |
| return getElement(); |
| } |
| } |
| |
| if (myFileReferenceSet.isUrlEncoded()) { |
| newName = encode(newName, element); |
| } |
| |
| return rename(newName); |
| } |
| |
| /** |
| * TODO: This should be fixed: bindToElement takes contexts from FileReferenceHelper.getContexts() while for resolve they are taken from |
| * FileReference.getContexts(). Note that in this case it should rename only the text range of the reference |
| */ |
| protected Collection<PsiFileSystemItem> getContextsForBindToElement(VirtualFile curVFile, Project project, FileReferenceHelper helper) { |
| return helper.getContexts(project, curVFile); |
| } |
| |
| protected PsiElement fixRefText(String name) { |
| return ElementManipulators.getManipulator(getElement()).handleContentChange(getElement(), getRangeInElement(), name); |
| } |
| |
| /* Happens when it's been moved to another folder */ |
| @Override |
| public PsiElement bindToElement(@NotNull final PsiElement element) throws IncorrectOperationException { |
| return bindToElement(element, myFileReferenceSet.isAbsolutePathReference()); |
| } |
| |
| protected PsiElement rename(final String newName) throws IncorrectOperationException { |
| final TextRange range = new TextRange(myFileReferenceSet.getStartInElement(), getRangeInElement().getEndOffset()); |
| PsiElement element = getElement(); |
| try { |
| return CachingReference.getManipulator(element).handleContentChange(element, range, newName); |
| } |
| catch (IncorrectOperationException e) { |
| LOG.error("Cannot rename " + getClass() + " from " + myFileReferenceSet.getClass() + " to " + newName, e); |
| throw e; |
| } |
| } |
| |
| protected static FileReferenceHelper[] getHelpers() { |
| return FileReferenceHelperRegistrar.getHelpers(); |
| } |
| |
| public int getIndex() { |
| return myIndex; |
| } |
| |
| @NotNull |
| @Override |
| public String getUnresolvedMessagePattern() { |
| return LangBundle.message("error.cannot.resolve") |
| + " " + (isLast() ? LangBundle.message("terms.file") : LangBundle.message("terms.directory")) |
| + " '" + StringUtil.escapePattern(decode(getCanonicalText())) + "'"; |
| } |
| |
| public final boolean isLast() { |
| return myIndex == myFileReferenceSet.getAllReferences().length - 1; |
| } |
| |
| @NotNull |
| public FileReferenceSet getFileReferenceSet() { |
| return myFileReferenceSet; |
| } |
| |
| @Override |
| public LocalQuickFix[] getQuickFixes() { |
| final List<LocalQuickFix> result = new ArrayList<LocalQuickFix>(); |
| for (final FileReferenceHelper helper : getHelpers()) { |
| result.addAll(helper.registerFixes(this)); |
| } |
| return result.toArray(new LocalQuickFix[result.size()]); |
| } |
| |
| @Override |
| public FileReference getLastFileReference() { |
| return myFileReferenceSet.getLastReference(); |
| } |
| |
| static class MyResolver implements ResolveCache.PolyVariantContextResolver<FileReference> { |
| static final MyResolver INSTANCE = new MyResolver(); |
| |
| @NotNull |
| @Override |
| public ResolveResult[] resolve(@NotNull FileReference ref, @NotNull PsiFile containingFile, boolean incompleteCode) { |
| return ref.innerResolve(ref.getFileReferenceSet().isCaseSensitive(), containingFile); |
| } |
| } |
| } |