/* * The MIT License (MIT) * * Copyright (c) 2017 hsz Jakub Chrzanowski <jakub@hsz.mobi> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package mobi.hsz.idea.gitignore.reference; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileVisitor; import com.intellij.psi.*; import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReference; import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet; import com.intellij.util.containers.ContainerUtil; import mobi.hsz.idea.gitignore.FilesIndexCacheProjectComponent; import mobi.hsz.idea.gitignore.psi.IgnoreEntry; import mobi.hsz.idea.gitignore.psi.IgnoreFile; import mobi.hsz.idea.gitignore.util.Constants; import mobi.hsz.idea.gitignore.util.Glob; import mobi.hsz.idea.gitignore.util.MatcherUtil; import mobi.hsz.idea.gitignore.util.Utils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * {@link FileReferenceSet} definition class. * * @author Alexander Zolotov <alexander.zolotov@jetbrains.com> * @author Jakub Chrzanowski <jakub@hsz.mobi> * @since 0.5 */ public class IgnoreReferenceSet extends FileReferenceSet { /** Instance of the Cache ProjectComponent that retrieves matching files using given {@link Pattern}. */ @NotNull private final FilesIndexCacheProjectComponent filesIndexCache; /** Constructor. */ public IgnoreReferenceSet(@NotNull IgnoreEntry element) { super(element); filesIndexCache = FilesIndexCacheProjectComponent.getInstance(element.getProject()); } /** * Creates {@link IgnoreReference} instance basing on passed text value. * * @param range text range * @param index start index * @param text string text * @return file reference */ @Override public FileReference createFileReference(TextRange range, int index, String text) { return new IgnoreReference(this, range, index, text); } /** * Sets ending slash as allowed. * * @return <code>false</code> */ @Override public boolean isEndingSlashNotAllowed() { return false; } /** * Computes current element's parent context. * * @return contexts collection */ @NotNull @Override public Collection<PsiFileSystemItem> computeDefaultContexts() { PsiFile containingFile = getElement().getContainingFile(); PsiDirectory containingDirectory = containingFile.getParent(); return containingDirectory != null ? Collections.<PsiFileSystemItem>singletonList(containingDirectory) : super.computeDefaultContexts(); } /** * Returns last reference of the current element's references. * * @return last {@link FileReference} */ @Nullable public FileReference getLastReference() { FileReference lastReference = super.getLastReference(); if (lastReference != null && lastReference.getCanonicalText().endsWith(getSeparatorString())) { return this.myReferences != null && this.myReferences.length > 1 ? this.myReferences[this.myReferences.length - 2] : null; } return lastReference; } /** * Disallows conversion to relative reference. * * @param relative is ignored * @return <code>false</code> */ @Override public boolean couldBeConvertedTo(boolean relative) { return false; } /** Parses entry, searches for file references and stores them in {@link #myReferences}. */ @Override protected void reparse() { String str = StringUtil.trimEnd(getPathString(), getSeparatorString()); final List<FileReference> referencesList = ContainerUtil.newArrayList(); String separatorString = getSeparatorString(); // separator's length can be more then 1 char int sepLen = separatorString.length(); int currentSlash = -sepLen; int startInElement = getStartInElement(); // skip white space while (currentSlash + sepLen < str.length() && Character.isWhitespace(str.charAt(currentSlash + sepLen))) { currentSlash++; } if (currentSlash + sepLen + sepLen < str.length() && str.substring(currentSlash + sepLen, currentSlash + sepLen + sepLen).equals(separatorString)) { currentSlash += sepLen; } int index = 0; if (str.equals(separatorString)) { final FileReference fileReference = createFileReference( new TextRange(startInElement, startInElement + sepLen), index++, separatorString ); referencesList.add(fileReference); } while (true) { final int nextSlash = str.indexOf(separatorString, currentSlash + sepLen); final String subReferenceText = nextSlash > 0 ? str.substring(0, nextSlash) : str; TextRange range = new TextRange(startInElement + currentSlash + sepLen, startInElement + (nextSlash > 0 ? nextSlash : str.length())); final FileReference ref = createFileReference(range, index++, subReferenceText); referencesList.add(ref); if ((currentSlash = nextSlash) < 0) { break; } } myReferences = referencesList.toArray(new FileReference[referencesList.size()]); } /** Custom definition of {@link FileReference}. */ private class IgnoreReference extends FileReference { /** Concurrent cache map. */ private final ConcurrentMap<String, Collection<VirtualFile>> cacheMap; /** Builds an instance of {@link IgnoreReferenceSet.IgnoreReference}. */ public IgnoreReference(@NotNull FileReferenceSet fileReferenceSet, TextRange range, int index, String text) { super(fileReferenceSet, range, index, text); cacheMap = ContainerUtil.newConcurrentMap(); } /** * Resolves reference to the filesystem. * * @param text entry * @param context filesystem context * @param result result references collection * @param caseSensitive is ignored */ @Override protected void innerResolveInContext(@NotNull String text, @NotNull PsiFileSystemItem context, @NotNull final Collection<ResolveResult> result, boolean caseSensitive) { super.innerResolveInContext(text, context, result, caseSensitive); final PsiFile containingFile = getContainingFile(); if (!(containingFile instanceof IgnoreFile)) { return; } VirtualFile contextVirtualFile; boolean isOuterFile = isOuterFile((IgnoreFile) containingFile); if (isOuterFile) { contextVirtualFile = getElement().getProject().getBaseDir(); result.clear(); } else if (Utils.isInProject(containingFile.getVirtualFile(), getElement().getProject())) { contextVirtualFile = context.getVirtualFile(); } else { return; } if (contextVirtualFile != null) { IgnoreEntry entry = (IgnoreEntry) getFileReferenceSet().getElement(); final Pattern pattern = Glob.createPattern(getCanonicalText(), entry.getSyntax()); if (pattern != null) { PsiDirectory parent = getElement().getContainingFile().getParent(); final VirtualFile root = isOuterFile ? contextVirtualFile : ((parent != null) ? parent.getVirtualFile() : null); final PsiManager manager = getElement().getManager(); final Matcher matcher = pattern.matcher(""); final List<VirtualFile> files = ContainerUtil.createLockFreeCopyOnWriteList(); files.addAll(filesIndexCache.getFilesForPattern(context.getProject(), pattern)); if (files.isEmpty()) { files.addAll(ContainerUtil.newArrayList(context.getVirtualFile().getChildren())); } else if (getCanonicalText().endsWith(Constants.DOUBLESTAR)) { final String key = entry.getText(); if (!cacheMap.containsKey(key)) { final Collection<VirtualFile> children = ContainerUtil.newArrayList(); final VirtualFileVisitor<?> visitor = new VirtualFileVisitor<Object>() { @Override public boolean visitFile(@NotNull VirtualFile file) { if (file.isDirectory()) { children.add(file); return true; } return false; } }; for (VirtualFile file : files) { if (!file.isDirectory()) { continue; } VfsUtil.visitChildrenRecursively(file, visitor); children.remove(file); } cacheMap.put(key, children); } files.clear(); files.addAll(cacheMap.get(key)); } for (VirtualFile file : files) { if (Utils.isVcsDirectory(file)) { continue; } String name = (root != null) ? Utils.getRelativePath(root, file) : file.getName(); if (MatcherUtil.match(matcher, name)) { PsiFileSystemItem psiFileSystemItem = getPsiFileSystemItem(manager, file); if (psiFileSystemItem == null) { continue; } result.add(new PsiElementResolveResult(psiFileSystemItem)); } } } } } /** * Checks if {@link IgnoreFile} is defined as an outer rules file. * * @param file current file * @return is outer file */ private boolean isOuterFile(@Nullable IgnoreFile file) { return file != null && file.isOuter(); } /** * Searches for directory or file using {@link PsiManager}. * * @param manager {@link PsiManager} instance * @param file working file * @return Psi item */ @Nullable private PsiFileSystemItem getPsiFileSystemItem(@NotNull PsiManager manager, @NotNull VirtualFile file) { if (!file.isValid()) { return null; } return file.isDirectory() ? manager.findDirectory(file) : manager.findFile(file); } } }