/* * Copyright 2000-2009 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.CodeInsightBundle; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementPresentation; import com.intellij.navigation.ChooseByNameContributor; import com.intellij.openapi.actionSystem.IdeActions; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileTypes.FileNameMatcher; import com.intellij.openapi.fileTypes.FileNameMatcherEx; import com.intellij.openapi.fileTypes.FileType; import com.intellij.openapi.fileTypes.FileTypeManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectFileIndex; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFileSystemItem; import com.intellij.psi.PsiReference; import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference; import com.intellij.psi.impl.source.resolve.reference.impl.providers.*; import com.intellij.psi.search.FilenameIndex; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.search.ProjectScope; import com.intellij.util.ArrayUtil; import com.intellij.util.ProcessingContext; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.util.*; import static com.intellij.patterns.PlatformPatterns.psiElement; /** * @author spleaner */ public class FilePathCompletionContributor extends CompletionContributor { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.FilePathCompletionContributor"); public FilePathCompletionContributor() { extend(CompletionType.BASIC, psiElement(), new CompletionProvider<CompletionParameters>() { @Override protected void addCompletions(@NotNull CompletionParameters parameters, ProcessingContext context, @NotNull CompletionResultSet result) { final PsiReference psiReference = parameters.getPosition().getContainingFile().findReferenceAt(parameters.getOffset()); if (getReference(psiReference) != null && parameters.getInvocationCount() == 1) { final String shortcut = getActionShortcut(IdeActions.ACTION_CODE_COMPLETION); result.addLookupAdvertisement(CodeInsightBundle.message("class.completion.file.path", shortcut)); } } }); CompletionProvider<CompletionParameters> provider = new CompletionProvider<CompletionParameters>() { @Override protected void addCompletions(@NotNull final CompletionParameters parameters, ProcessingContext context, @NotNull final CompletionResultSet _result) { if (!parameters.isExtendedCompletion()) { return; } @NotNull final CompletionResultSet result = _result.caseInsensitive(); final PsiElement e = parameters.getPosition(); final Project project = e.getProject(); final PsiReference psiReference = parameters.getPosition().getContainingFile().findReferenceAt(parameters.getOffset()); final Pair<FileReference, Boolean> fileReferencePair = getReference(psiReference); if (fileReferencePair != null) { final FileReference first = fileReferencePair.getFirst(); if (first == null) return; final FileReferenceSet set = first.getFileReferenceSet(); String prefix = set.getPathString() .substring(0, parameters.getOffset() - set.getElement().getTextRange().getStartOffset() - set.getStartInElement()); List<String> pathPrefixParts = null; int lastSlashIndex; if ((lastSlashIndex = prefix.lastIndexOf('/')) != -1) { pathPrefixParts = StringUtil.split(prefix.substring(0, lastSlashIndex), "/"); prefix = prefix.substring(lastSlashIndex + 1); } final CompletionResultSet __result = result.withPrefixMatcher(prefix).caseInsensitive(); final PsiFile originalFile = parameters.getOriginalFile(); final VirtualFile contextFile = originalFile.getVirtualFile(); if (contextFile != null) { final String[] fileNames = getAllNames(project); final Set<String> resultNames = new TreeSet<>(); for (String fileName : fileNames) { if (filenameMatchesPrefixOrType(fileName, prefix, set.getSuitableFileTypes(), parameters.getInvocationCount())) { resultNames.add(fileName); } } final ProjectFileIndex index = ProjectRootManager.getInstance(project).getFileIndex(); final Module contextModule = index.getModuleForFile(contextFile); if (contextModule != null) { final List<FileReferenceHelper> helpers = FileReferenceHelperRegistrar.getHelpers(originalFile); final GlobalSearchScope scope = ProjectScope.getProjectScope(project); for (final String name : resultNames) { ProgressManager.checkCanceled(); final PsiFile[] files = FilenameIndex.getFilesByName(project, name, scope); if (files.length <= 0) { continue; } for (final PsiFile file : files) { ProgressManager.checkCanceled(); final VirtualFile virtualFile = file.getVirtualFile(); if (virtualFile == null || !virtualFile.isValid() || Comparing.equal(virtualFile, contextFile)) { continue; } List<FileReferenceHelper> helperList = new ArrayList<>(); for (FileReferenceHelper contextHelper : helpers) { ProgressManager.checkCanceled(); if (contextHelper.isMine(project, virtualFile)) { if (pathPrefixParts == null || fileMatchesPathPrefix(contextHelper.getPsiFileSystemItem(project, virtualFile), pathPrefixParts)) { helperList.add(contextHelper); } } } if (!helperList.isEmpty()) { __result.addElement(new FilePathLookupItem(file, helperList)); } } } } } if (set.getSuitableFileTypes().length > 0 && parameters.getInvocationCount() == 1) { final String shortcut = getActionShortcut(IdeActions.ACTION_CODE_COMPLETION); result.addLookupAdvertisement(CodeInsightBundle.message("class.completion.file.path.all.variants", shortcut)); } if (fileReferencePair.getSecond()) result.stopHere(); } } }; extend(CompletionType.BASIC, psiElement(), provider); } private static boolean filenameMatchesPrefixOrType(final String fileName, final String prefix, final FileType[] suitableFileTypes, final int invocationCount) { final boolean prefixMatched = prefix.length() == 0 || StringUtil.startsWithIgnoreCase(fileName, prefix); if (prefixMatched && (suitableFileTypes.length == 0 || invocationCount > 2)) return true; if (prefixMatched) { final String extension = FileUtilRt.getExtension(fileName); if (extension.length() == 0) return false; for (final FileType fileType : suitableFileTypes) { for (final FileNameMatcher matcher : FileTypeManager.getInstance().getAssociations(fileType)) { if (FileNameMatcherEx.acceptsCharSequence(matcher, fileName)) return true; } } } return false; } private static boolean fileMatchesPathPrefix(@Nullable final PsiFileSystemItem file, @NotNull final List<String> pathPrefix) { if (file == null) return false; final List<String> contextParts = new ArrayList<>(); PsiFileSystemItem parentFile = file; PsiFileSystemItem parent; while ((parent = parentFile.getParent()) != null) { if (parent.getName().length() > 0) contextParts.add(0, parent.getName().toLowerCase()); parentFile = parent; } final String path = StringUtil.join(contextParts, "/"); int nextIndex = 0; for (@NonNls final String s : pathPrefix) { if ((nextIndex = path.indexOf(s.toLowerCase(), nextIndex)) == -1) return false; } return true; } private static String[] getAllNames(@NotNull final Project project) { Set<String> names = new HashSet<>(); final ChooseByNameContributor[] nameContributors = ChooseByNameContributor.FILE_EP_NAME.getExtensions(); for (final ChooseByNameContributor contributor : nameContributors) { try { names.addAll(Arrays.asList(contributor.getNames(project, false))); } catch (ProcessCanceledException ex) { // index corruption detected, ignore } catch (Exception ex) { LOG.error(ex); } } return ArrayUtil.toStringArray(names); } @Nullable private static Pair<FileReference, Boolean> getReference(final PsiReference original) { if (original == null) { return null; } if (original instanceof PsiMultiReference) { final PsiMultiReference multiReference = (PsiMultiReference)original; for (PsiReference reference : multiReference.getReferences()) { if (reference instanceof FileReference) { if (((FileReference)reference).getFileReferenceSet().supportsExtendedCompletion()) { return Pair.create((FileReference)reference, false); } } } } else if (original instanceof FileReferenceOwner) { final PsiFileReference fileReference = ((FileReferenceOwner)original).getLastFileReference(); if (fileReference instanceof FileReference) { if (((FileReference)fileReference).getFileReferenceSet().supportsExtendedCompletion()) { return Pair.create((FileReference) fileReference, true); } } } return null; } public static class FilePathLookupItem extends LookupElement { private final String myName; private final String myPath; private final String myInfo; private final Icon myIcon; private final PsiFile myFile; private final List<FileReferenceHelper> myHelpers; public FilePathLookupItem(@NotNull final PsiFile file, @NotNull final List<FileReferenceHelper> helpers) { myName = file.getName(); myPath = file.getVirtualFile().getPath(); myHelpers = helpers; myInfo = FileInfoManager.getFileAdditionalInfo(file); myIcon = file.getFileType().getIcon(); myFile = file; } @SuppressWarnings({"HardCodedStringLiteral"}) @Override public String toString() { return String.format("%s%s", myName, myInfo == null ? "" : " (" + myInfo + ")"); } @NotNull @Override public Object getObject() { return myFile; } @Override @NotNull public String getLookupString() { return myName; } @Override public void handleInsert(InsertionContext context) { context.commitDocument(); if (myFile.isValid()) { final PsiReference psiReference = context.getFile().findReferenceAt(context.getStartOffset()); final Pair<FileReference, Boolean> fileReferencePair = getReference(psiReference); if (fileReferencePair != null) { FileReference ref = fileReferencePair.getFirst(); context.setTailOffset(ref.getRangeInElement().getEndOffset() + ref.getElement().getTextRange().getStartOffset()); ref.bindToElement(myFile); } } } @Override public void renderElement(LookupElementPresentation presentation) { final String relativePath = getRelativePath(); final StringBuilder sb = new StringBuilder(); if (myInfo != null) { sb.append(" (").append(myInfo); } if (relativePath != null && !relativePath.equals(myName)) { if (myInfo != null) { sb.append(", "); } else { sb.append(" ("); } sb.append(relativePath); } if (sb.length() > 0) { sb.append(')'); } presentation.setItemText(myName); if (sb.length() > 0) { presentation.setTailText(sb.toString(), true); } presentation.setIcon(myIcon); } @Nullable private String getRelativePath() { final VirtualFile virtualFile = myFile.getVirtualFile(); LOG.assertTrue(virtualFile != null); for (FileReferenceHelper helper : myHelpers) { final PsiFileSystemItem root = helper.findRoot(myFile.getProject(), virtualFile); String path = PsiFileSystemItemUtil.getRelativePath(root, helper.getPsiFileSystemItem(myFile.getProject(), virtualFile)); if (path != null) return path; } return null; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; FilePathLookupItem that = (FilePathLookupItem)o; if (!myName.equals(that.myName)) return false; if (!myPath.equals(that.myPath)) return false; return true; } @Override public int hashCode() { int result = myName.hashCode(); result = 31 * result + myPath.hashCode(); return result; } } }