package fr.adrienbrault.idea.symfony2plugin.util.resource;
import com.intellij.codeInsight.daemon.RelatedItemLineMarkerInfo;
import com.intellij.codeInsight.navigation.NavigationGutterIconBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NotNullLazyValue;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.indexing.FileBasedIndex;
import com.jetbrains.php.PhpIcons;
import com.jetbrains.php.PhpIndex;
import fr.adrienbrault.idea.symfony2plugin.stubs.indexes.FileResourcesIndex;
import fr.adrienbrault.idea.symfony2plugin.util.FileResourceVisitorUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PhpIndexUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyBundleUtil;
import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyBundle;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Daniel Espendiller <daniel@espendiller.net>
*/
public class FileResourceUtil {
/**
* Search for files refers to given file
*/
@NotNull
public static Collection<VirtualFile> getFileResourceRefers(@NotNull Project project, @NotNull VirtualFile virtualFile) {
String bundleLocateName = getBundleLocateName(project, virtualFile);
if(bundleLocateName == null) {
return Collections.emptyList();
}
return getFileResourceRefers(project, bundleLocateName);
}
/**
* Search for files refers to given file
*/
@NotNull
public static Collection<VirtualFile> getFileResourceRefers(@NotNull Project project, @NotNull String bundleLocateName) {
return FileBasedIndex.getInstance().getContainingFiles(
FileResourcesIndex.KEY,
bundleLocateName,
GlobalSearchScope.allScope(project)
);
}
@Nullable
public static String getBundleLocateName(@NotNull Project project, @NotNull VirtualFile virtualFile) {
SymfonyBundle containingBundle = new SymfonyBundleUtil(project).getContainingBundle(virtualFile);
if(containingBundle == null) {
return null;
}
String relativePath = containingBundle.getRelativePath(virtualFile);
if(relativePath == null) {
return null;
}
return "@" + containingBundle.getName() + "/" + relativePath;
}
/**
* Search for line definition of "@FooBundle/foo.xml"
*/
@NotNull
private static Collection<PsiElement> getBundleLocateStringDefinitions(@NotNull Project project, final @NotNull String bundleFileName) {
final Collection<PsiElement> psiElements = new HashSet<>();
for (VirtualFile refVirtualFile : getFileResourceRefers(project, bundleFileName)) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(refVirtualFile);
if(psiFile == null) {
continue;
}
FileResourceVisitorUtil.visitFile(psiFile, consumer -> {
if (bundleFileName.equals(consumer.getResource())) {
psiElements.add(consumer.getPsiElement());
}
});
}
return psiElements;
}
@Nullable
public static RelatedItemLineMarkerInfo<PsiElement> getFileImplementsLineMarker(@NotNull PsiFile psiFile) {
final Project project = psiFile.getProject();
VirtualFile virtualFile = psiFile.getVirtualFile();
if(virtualFile == null) {
return null;
}
String bundleLocateName = FileResourceUtil.getBundleLocateName(project, virtualFile);
if(bundleLocateName == null) {
return null;
}
if(FileResourceUtil.getFileResourceRefers(project, bundleLocateName).size() == 0) {
return null;
}
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(PhpIcons.IMPLEMENTS).
setTargets(new FileResourceUtil.FileResourceNotNullLazyValue(project, bundleLocateName)).
setTooltipText("Navigate to resource");
return builder.createLineMarkerInfo(psiFile);
}
/**
* On route annotations we can have folder scope so: "@FooBundle/Controller/foo.php" can be equal "@FooBundle/Controller/"
*/
@Nullable
public static RelatedItemLineMarkerInfo<PsiElement> getFileImplementsLineMarkerInFolderScope(@NotNull PsiFile psiFile) {
VirtualFile virtualFile = psiFile.getVirtualFile();
if(virtualFile == null) {
return null;
}
final Project project = psiFile.getProject();
String bundleLocateName = FileResourceUtil.getBundleLocateName(project, virtualFile);
if(bundleLocateName == null) {
return null;
}
Set<String> names = new HashSet<>();
names.add(bundleLocateName);
// strip filename
int i = bundleLocateName.lastIndexOf("/");
if(i > 0) {
names.add(bundleLocateName.substring(0, i));
}
int targets = 0;
for (String name : names) {
targets += FileResourceUtil.getFileResourceRefers(project, name).size();
}
if(targets == 0) {
return null;
}
NavigationGutterIconBuilder<PsiElement> builder = NavigationGutterIconBuilder.create(PhpIcons.IMPLEMENTS).
setTargets(new FileResourceUtil.FileResourceNotNullLazyValue(project, names)).
setTooltipText("Navigate to resource");
return builder.createLineMarkerInfo(psiFile);
}
private static class FileResourceNotNullLazyValue extends NotNullLazyValue<Collection<? extends PsiElement>> {
private final Collection<String> resources;
private final Project project;
public FileResourceNotNullLazyValue(@NotNull Project project, @NotNull Collection<String> resource) {
this.resources = resource;
this.project = project;
}
public FileResourceNotNullLazyValue(@NotNull Project project, @NotNull String resource) {
this.resources = Collections.singleton(resource);
this.project = project;
}
@NotNull
@Override
protected Collection<? extends PsiElement> compute() {
Collection<PsiElement> psiElements = new HashSet<>();
for (String resource : this.resources) {
psiElements.addAll(getBundleLocateStringDefinitions(project, resource));
}
return psiElements;
}
}
/**
* Gives targets to files on Bundle locate syntax. "@FooBundle/.../foo.yml"
*/
@NotNull
public static Collection<PsiFile> getFileResourceTargetsInBundleScope(@NotNull Project project, @NotNull String content) {
// min validation "@FooBundle/foo.yml"
if(!content.startsWith("@") || !content.contains("/")) {
return Collections.emptyList();
}
String bundleName = content.substring(1, content.indexOf("/"));
SymfonyBundle symfonyBundle = new SymfonyBundleUtil(PhpIndex.getInstance(project)).getBundle(bundleName);
if(symfonyBundle == null) {
return Collections.emptyList();
}
String path = content.substring(content.indexOf("/") + 1);
PsiFile psiFile = PsiElementUtils.virtualFileToPsiFile(project, symfonyBundle.getRelative(path));
if(psiFile == null) {
return Collections.emptyList();
}
return Collections.singletonList(psiFile);
}
/**
* resource: "@AppBundle/Controller/"
*/
@NotNull
public static Collection<PsiElement> getFileResourceTargetsInBundleDirectory(@NotNull Project project, @NotNull String content) {
// min validation "@FooBundle/foo.yml"
if(!content.startsWith("@")) {
return Collections.emptyList();
}
content = content.replace("/", "\\");
if(!content.contains("\\")) {
return Collections.emptyList();
}
String bundleName = content.substring(1, content.indexOf("\\"));
SymfonyBundle symfonyBundle = new SymfonyBundleUtil(PhpIndex.getInstance(project)).getBundle(bundleName);
if(symfonyBundle == null) {
return Collections.emptyList();
}
// support double backslashes
content = content.replaceAll("\\\\+", "\\\\");
String namespaceName = "\\" + StringUtils.strip(content.substring(1), "\\");
return new ArrayList<>(PhpIndexUtil.getPhpClassInsideNamespace(
project, namespaceName
));
}
/**
* Gives targets to files which relative to current file directory
*/
@NotNull
public static Collection<PsiFile> getFileResourceTargetsInDirectoryScope(@NotNull PsiFile psiFile, @NotNull String content) {
// bundle scope
if(content.startsWith("@")) {
return Collections.emptyList();
}
PsiDirectory containingDirectory = psiFile.getContainingDirectory();
if(containingDirectory == null) {
return Collections.emptyList();
}
VirtualFile relativeFile = VfsUtil.findRelativeFile(content, containingDirectory.getVirtualFile());
if(relativeFile == null) {
return Collections.emptyList();
}
PsiFile targetFile = PsiElementUtils.virtualFileToPsiFile(psiFile.getProject(), relativeFile);
if(targetFile == null) {
return Collections.emptyList();
}
return Collections.singletonList(targetFile);
}
}