/*
* Copyright 2000-2016 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.lang.injection.InjectedLanguageManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Conditions;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.util.Function;
import com.intellij.util.NullableFunction;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Maxim.Mossienko
*/
public class FileReferenceSet {
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceSet");
private static final FileType[] EMPTY_FILE_TYPES = {};
public static final CustomizableReferenceProvider.CustomizationKey<Function<PsiFile, Collection<PsiFileSystemItem>>> DEFAULT_PATH_EVALUATOR_OPTION =
new CustomizableReferenceProvider.CustomizationKey<>(PsiBundle.message("default.path.evaluator.option"));
public static final Function<PsiFile, Collection<PsiFileSystemItem>> ABSOLUTE_TOP_LEVEL = file -> getAbsoluteTopLevelDirLocations(file);
public static final Condition<PsiFileSystemItem> FILE_FILTER = item -> item instanceof PsiFile;
public static final Condition<PsiFileSystemItem> DIRECTORY_FILTER = item -> item instanceof PsiDirectory;
protected FileReference[] myReferences;
private PsiElement myElement;
private final int myStartInElement;
private final boolean myCaseSensitive;
private final String myPathStringNonTrimmed;
private final String myPathString;
private Collection<PsiFileSystemItem> myDefaultContexts;
private final boolean myEndingSlashNotAllowed;
private boolean myEmptyPathAllowed;
@Nullable private Map<CustomizableReferenceProvider.CustomizationKey, Object> myOptions;
@Nullable private FileType[] mySuitableFileTypes;
public FileReferenceSet(String str,
@NotNull PsiElement element,
int startInElement,
PsiReferenceProvider provider,
boolean caseSensitive,
boolean endingSlashNotAllowed,
@Nullable FileType[] suitableFileTypes) {
this(str, element, startInElement, provider, caseSensitive, endingSlashNotAllowed, suitableFileTypes, true);
}
public FileReferenceSet(String str,
@NotNull PsiElement element,
int startInElement,
PsiReferenceProvider provider,
boolean caseSensitive,
boolean endingSlashNotAllowed,
@Nullable FileType[] suitableFileTypes,
boolean init) {
myElement = element;
myStartInElement = startInElement;
myCaseSensitive = caseSensitive;
myPathStringNonTrimmed = str;
myPathString = str.trim();
myEndingSlashNotAllowed = endingSlashNotAllowed;
myEmptyPathAllowed = !endingSlashNotAllowed;
myOptions = provider instanceof CustomizableReferenceProvider ? ((CustomizableReferenceProvider)provider).getOptions() : null;
mySuitableFileTypes = suitableFileTypes;
if (init) {
reparse();
}
}
protected String getNewAbsolutePath(PsiFileSystemItem root, String relativePath) {
return absoluteUrlNeedsStartSlash() ? "/" + relativePath : relativePath;
}
public String getSeparatorString() {
return "/";
}
protected int findSeparatorLength(@NotNull CharSequence sequence, int atOffset) {
return StringUtil.startsWith(sequence, atOffset, getSeparatorString()) ? getSeparatorString().length() : 0;
}
protected int findSeparatorOffset(@NotNull CharSequence sequence, int startingFrom) {
return StringUtil.indexOf(sequence, getSeparatorString(), startingFrom);
}
/**
* This should be removed. Please use {@link FileReference#getContexts()} instead.
*/
@Deprecated
protected Collection<PsiFileSystemItem> getExtraContexts() {
return Collections.emptyList();
}
public static FileReferenceSet createSet(@NotNull PsiElement element, final boolean soft, boolean endingSlashNotAllowed, final boolean urlEncoded) {
final ElementManipulator<PsiElement> manipulator = ElementManipulators.getManipulator(element);
assert manipulator != null;
final TextRange range = manipulator.getRangeInElement(element);
int offset = range.getStartOffset();
String text = range.substring(element.getText());
for (final FileReferenceHelper helper : FileReferenceHelperRegistrar.getHelpers()) {
text = helper.trimUrl(text);
}
return new FileReferenceSet(text, element, offset, null, true, endingSlashNotAllowed) {
@Override
protected boolean isUrlEncoded() {
return urlEncoded;
}
@Override
protected boolean isSoft() {
return soft;
}
};
}
public FileReferenceSet(String str, @NotNull PsiElement element, int startInElement, @Nullable PsiReferenceProvider provider, final boolean isCaseSensitive) {
this(str, element, startInElement, provider, isCaseSensitive, true);
}
public FileReferenceSet(@NotNull String str,
@NotNull PsiElement element,
int startInElement,
PsiReferenceProvider provider,
final boolean isCaseSensitive,
boolean endingSlashNotAllowed) {
this(str, element, startInElement, provider, isCaseSensitive, endingSlashNotAllowed, null);
}
public FileReferenceSet(@NotNull final PsiElement element) {
myElement = element;
TextRange range = ElementManipulators.getValueTextRange(element);
myStartInElement = range.getStartOffset();
myPathStringNonTrimmed = range.substring(element.getText());
myPathString = myPathStringNonTrimmed.trim();
myEndingSlashNotAllowed = true;
myCaseSensitive = false;
reparse();
}
@NotNull
public PsiElement getElement() {
return myElement;
}
void setElement(@NotNull PsiElement element) {
myElement = element;
}
public boolean isCaseSensitive() {
return myCaseSensitive;
}
public boolean isEndingSlashNotAllowed() {
return myEndingSlashNotAllowed;
}
public int getStartInElement() {
return myStartInElement;
}
public FileReference createFileReference(final TextRange range, final int index, final String text) {
return new FileReference(this, range, index, text);
}
protected void reparse() {
List<FileReference> referencesList = reparse(myPathStringNonTrimmed, myStartInElement);
myReferences = referencesList.toArray(new FileReference[referencesList.size()]);
}
protected List<FileReference> reparse(String str, int startInElement) {
int wsHead = 0;
int wsTail = 0;
LiteralTextEscaper<? extends PsiLanguageInjectionHost> escaper;
TextRange valueRange;
CharSequence decoded;
// todo replace @param str with honest @param rangeInElement; and drop the following startsWith(..)
if (myElement instanceof PsiLanguageInjectionHost && !StringUtil.startsWith(myElement.getText(), startInElement, str)) {
escaper = ((PsiLanguageInjectionHost)myElement).createLiteralTextEscaper();
valueRange = ElementManipulators.getValueTextRange(myElement);
StringBuilder sb = new StringBuilder();
escaper.decode(valueRange, sb);
decoded = sb;
wsHead += Math.max(0, startInElement - valueRange.getStartOffset());
}
else {
escaper = null;
decoded = str;
valueRange = TextRange.from(startInElement, decoded.length());
}
List<FileReference> referencesList = ContainerUtil.newArrayList();
for (int i = wsHead; i < decoded.length() && Character.isWhitespace(decoded.charAt(i)); i++) {
wsHead++; // skip head white spaces
}
for (int i = decoded.length() - 1; i >= 0 && Character.isWhitespace(decoded.charAt(i)); i--) {
wsTail++; // skip tail white spaces
}
int index = 0;
int curSep = findSeparatorOffset(decoded, wsHead);
int sepLen = curSep >= wsHead ? findSeparatorLength(decoded, curSep) : 0;
if (curSep >= 0 && decoded.length() == wsHead + sepLen + wsTail) {
// add extra reference for the only & leading "/"
TextRange r = TextRange.create(startInElement, offset(curSep + Math.max(0, sepLen - 1), escaper, valueRange) + 1);
referencesList.add(createFileReference(r, index++, decoded.subSequence(curSep, curSep + sepLen).toString()));
}
curSep = curSep == wsHead ? curSep + sepLen : wsHead; // reset offsets & start again for simplicity
sepLen = 0;
while (curSep >= 0) {
int nextSep = findSeparatorOffset(decoded, curSep + sepLen);
int start = curSep + sepLen;
int endTrimmed = nextSep > 0 ? nextSep : Math.max(start, decoded.length() - wsTail);
int endInclusive = nextSep > 0 ? nextSep : Math.max(start, decoded.length() - 1 - wsTail);
// todo move ${placeholder} support (the str usage below) to a reference implementation
// todo reference-set should be bound to exact range & text in a file, consider: ${slash}path${slash}file&.txt
String refText = index == 0 && nextSep < 0 && !StringUtil.contains(decoded, str) ? str : decoded.subSequence(start, endTrimmed).toString();
TextRange r =
new TextRange(offset(start, escaper, valueRange), offset(endInclusive, escaper, valueRange) + (nextSep < 0 && refText.length() > 0 ? 1 : 0));
referencesList.add(createFileReference(r, index++, refText));
curSep = nextSep;
sepLen = curSep > 0 ? findSeparatorLength(decoded, curSep) : 0;
}
return referencesList;
}
private static int offset(int offset, LiteralTextEscaper<? extends PsiLanguageInjectionHost> escaper, TextRange valueRange) {
return escaper == null ? offset + valueRange.getStartOffset() : escaper.getOffsetInHost(offset, valueRange);
}
public FileReference getReference(int index) {
return myReferences[index];
}
@NotNull
public FileReference[] getAllReferences() {
return myReferences;
}
protected boolean isSoft() {
return false;
}
protected boolean isUrlEncoded() {
return false;
}
@NotNull
public Collection<PsiFileSystemItem> getDefaultContexts() {
if (myDefaultContexts == null) {
myDefaultContexts = computeDefaultContexts();
}
return myDefaultContexts;
}
@NotNull
public Collection<PsiFileSystemItem> computeDefaultContexts() {
final PsiFile file = getContainingFile();
if (file == null) return Collections.emptyList();
if (myOptions != null) {
final Function<PsiFile, Collection<PsiFileSystemItem>> value = DEFAULT_PATH_EVALUATOR_OPTION.getValue(myOptions);
if (value != null) {
final Collection<PsiFileSystemItem> roots = value.fun(file);
if (roots != null) {
for (PsiFileSystemItem root : roots) {
if (root == null) {
LOG.error("Default path evaluator " + value + " produced a null root for " + file);
}
}
return roots;
}
}
}
if (isAbsolutePathReference()) {
return getAbsoluteTopLevelDirLocations(file);
}
return getContextByFile(file);
}
@Nullable
protected PsiFile getContainingFile() {
PsiFile cf = myElement.getContainingFile();
PsiFile file = InjectedLanguageManager.getInstance(cf.getProject()).getTopLevelFile(cf);
if (file != null) return file.getOriginalFile();
LOG.error("Invalid element: " + myElement);
return null;
}
@NotNull
private Collection<PsiFileSystemItem> getContextByFile(@NotNull PsiFile file) {
final PsiElement context = file.getContext();
if (context != null) file = context.getContainingFile();
if (useIncludingFileAsContext()) {
final FileContextProvider contextProvider = FileContextProvider.getProvider(file);
if (contextProvider != null) {
final Collection<PsiFileSystemItem> folders = contextProvider.getContextFolders(file);
if (!folders.isEmpty()) {
return folders;
}
final PsiFile contextFile = contextProvider.getContextFile(file);
if (contextFile != null) {
return Collections.singleton(contextFile.getParent());
}
}
}
return getContextByFileSystemItem(file.getOriginalFile());
}
@NotNull
protected Collection<PsiFileSystemItem> getContextByFileSystemItem(@NotNull PsiFileSystemItem file) {
VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile != null) {
final FileReferenceHelper[] helpers = FileReferenceHelperRegistrar.getHelpers();
final ArrayList<PsiFileSystemItem> list = new ArrayList<>();
final Project project = file.getProject();
for (FileReferenceHelper helper : helpers) {
if (helper.isMine(project, virtualFile)) {
if (!list.isEmpty() && helper.isFallback()) {
continue;
}
list.addAll(helper.getContexts(project, virtualFile));
}
}
if (!list.isEmpty()) {
return list;
}
return getParentDirectoryContext();
}
return Collections.emptyList();
}
@NotNull
protected Collection<PsiFileSystemItem> getParentDirectoryContext() {
PsiFile file = getContainingFile();
VirtualFile virtualFile = file == null ? null : file.getOriginalFile().getVirtualFile();
final VirtualFile parent = virtualFile == null ? null : virtualFile.getParent();
final PsiDirectory directory = parent == null ? null : file.getManager().findDirectory(parent);
return directory != null ? Collections.singleton(directory) : Collections.emptyList();
}
public String getPathString() {
return myPathString;
}
public boolean isAbsolutePathReference() {
return myPathString.startsWith(getSeparatorString());
}
protected boolean useIncludingFileAsContext() {
return true;
}
@Nullable
public PsiFileSystemItem resolve() {
final FileReference lastReference = getLastReference();
return lastReference == null ? null : lastReference.resolve();
}
@Nullable
public FileReference getLastReference() {
return myReferences == null || myReferences.length == 0 ? null : myReferences[myReferences.length - 1];
}
@NotNull
public static Collection<PsiFileSystemItem> getAbsoluteTopLevelDirLocations(@NotNull final PsiFile file) {
final VirtualFile virtualFile = file.getVirtualFile();
if (virtualFile == null) return Collections.emptyList();
final PsiDirectory parent = file.getParent();
final Module module = ModuleUtilCore.findModuleForPsiElement(parent == null ? file : parent);
if (module == null) return Collections.emptyList();
final List<PsiFileSystemItem> list = new ArrayList<>();
final Project project = file.getProject();
for (FileReferenceHelper helper : FileReferenceHelperRegistrar.getHelpers()) {
if (helper.isMine(project, virtualFile)) {
if (helper.isFallback() && !list.isEmpty()) {
continue;
}
final Collection<PsiFileSystemItem> roots = helper.getRoots(module);
for (PsiFileSystemItem root : roots) {
if (root == null) {
LOG.error("Helper " + helper + " produced a null root for " + file);
}
}
list.addAll(roots);
}
}
return list;
}
@NotNull
protected Collection<PsiFileSystemItem> toFileSystemItems(VirtualFile... files) {
return toFileSystemItems(Arrays.asList(files));
}
@NotNull
protected Collection<PsiFileSystemItem> toFileSystemItems(@NotNull Collection<VirtualFile> files) {
final PsiManager manager = getElement().getManager();
return ContainerUtil.mapNotNull(files, (NullableFunction<VirtualFile, PsiFileSystemItem>)file -> file != null ? manager.findDirectory(file) : null);
}
protected Condition<PsiFileSystemItem> getReferenceCompletionFilter() {
return Conditions.alwaysTrue();
}
public <Option> void addCustomization(CustomizableReferenceProvider.CustomizationKey<Option> key, Option value) {
if (myOptions == null) {
myOptions = new HashMap<>(5);
}
myOptions.put(key, value);
}
public boolean couldBeConvertedTo(final boolean relative) {
return true;
}
public boolean absoluteUrlNeedsStartSlash() {
return true;
}
@NotNull
public FileType[] getSuitableFileTypes() {
return mySuitableFileTypes == null ? EMPTY_FILE_TYPES : mySuitableFileTypes;
}
public boolean isEmptyPathAllowed() {
return myEmptyPathAllowed;
}
public void setEmptyPathAllowed(boolean emptyPathAllowed) {
myEmptyPathAllowed = emptyPathAllowed;
}
public boolean supportsExtendedCompletion() {
return true;
}
}