Skip to content

Commit

Permalink
Merge pull request #2254 from Haehnchen/feature/paid-asset-mapper
Browse files Browse the repository at this point in the history
add support for AssetMapper and importmap
  • Loading branch information
Haehnchen authored Nov 30, 2023
2 parents b1b9544 + b2b9130 commit 457a4d8
Show file tree
Hide file tree
Showing 11 changed files with 543 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package fr.adrienbrault.idea.symfony2plugin.assetMapper;

import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementBuilder;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.search.FilenameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.*;
import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression;
import com.jetbrains.php.lang.psi.elements.PhpReturn;
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons;
import fr.adrienbrault.idea.symfony2plugin.assetMapper.dict.AssetMapperModule;
import fr.adrienbrault.idea.symfony2plugin.assetMapper.dict.MappingFileEnum;
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil;
import fr.adrienbrault.idea.symfony2plugin.util.ProjectUtil;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* @author Daniel Espendiller <[email protected]>
*/
public class AssetMapperUtil {

private static final Key<CachedValue<List<AssetMapperModule>>> MAPPING_CACHE = new Key<>("SYMFONY_ASSET_MAPPER_MAPPING_CACHE");

public static List<AssetMapperModule> getMappingFiles(@NotNull Project project) {
return CachedValuesManager.getManager(project).getCachedValue(
project,
MAPPING_CACHE,
() -> CachedValueProvider.Result.create(getMappingFilesInner(project), PsiModificationTracker.MODIFICATION_COUNT),
false
);
}

@NotNull
private static List<AssetMapperModule> getMappingFilesInner(@NotNull Project project) {
List<AssetMapperModule> modules = new ArrayList<>();

Set<VirtualFile> files = new LinkedHashSet<>();

VirtualFile importmapFile = VfsUtil.findRelativeFile(ProjectUtil.getProjectDir(project), "importmap.php");
if (importmapFile != null) {
files.add(importmapFile);
}

VirtualFile installedFile = VfsUtil.findRelativeFile(ProjectUtil.getProjectDir(project), "assets", "vendor", "installed.php");
if (installedFile != null) {
files.add(installedFile);
}

files.addAll(FilenameIndex.getVirtualFilesByName("importmap.php", GlobalSearchScope.allScope(project)));
for (VirtualFile file : FilenameIndex.getVirtualFilesByName("installed.php", GlobalSearchScope.allScope(project))) {
// composer
VirtualFile parent = file.getParent();
if (parent != null && "composer".equals(parent.getName())) {
continue;
}

files.add(file);
}

for (VirtualFile file : files) {
PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
if (psiFile == null) {
continue;
}

for (PhpReturn phpReturn : PsiTreeUtil.collectElementsOfType(psiFile, PhpReturn.class)) {
PsiElement argument = phpReturn.getArgument();
if (argument instanceof ArrayCreationExpression arrayCreationExpression) {
for (Map.Entry<String, PsiElement> entry : PhpElementsUtil.getArrayKeyValueMapWithValueAsPsiElement(arrayCreationExpression).entrySet()) {
String path = null;
String url = null;
String version = null;
Boolean entrypoint = false;
String type = null;

PsiElement value = entry.getValue();
if (value instanceof ArrayCreationExpression value2) {
path = PhpElementsUtil.getArrayValueString(value2, "path");
url = PhpElementsUtil.getArrayValueString(value2, "url");
version = PhpElementsUtil.getArrayValueString(value2, "version");
entrypoint = PhpElementsUtil.getArrayValueBool(value2, "entrypoint");
type = PhpElementsUtil.getArrayValueString(value2, "type");
}

modules.add(new AssetMapperModule(MappingFileEnum.fromString(file.getName()), file, entry.getKey(), path, url, version, entrypoint, type));
}
}
}
}

return modules;
}

@NotNull
private static Collection<VirtualFile> getModuleReferences(@NotNull String module, @NotNull List<AssetMapperModule> modules) {
Collection<VirtualFile> files = new HashSet<>();

for (AssetMapperModule mappingFile : modules) {
if (!mappingFile.key().equals(module)) {
continue;
}

if (mappingFile.sourceType() == MappingFileEnum.IMPORTMAP) {
// default project structure: "assets/vendor/*"
VirtualFile parent = mappingFile.sourceFile().getParent();
if (parent != null) {
if (mappingFile.path() != null) {
// simple path normalize: "./app.js"
String[] split = Arrays.stream(StringUtils.split(mappingFile.path(), "/"))
.filter(s -> !s.equals("."))
.toArray(String[]::new);

VirtualFile relativeFile = VfsUtil.findRelativeFile(parent, split);
if (relativeFile != null) {
files.add(relativeFile);
break;
}
} else {
if ("css".equals(mappingFile.type())) {
String[] split = StringUtils.split("assets/vendor/" + mappingFile.key(), "/");
VirtualFile relativeFile = VfsUtil.findRelativeFile(parent, split);
if (relativeFile != null) {
files.add(relativeFile);
break;
}
} else if (mappingFile.key().contains("/")) {
String[] split = StringUtils.split("assets/vendor/" + mappingFile.key() + ".js", "/");
VirtualFile relativeFile = VfsUtil.findRelativeFile(parent, split);
if (relativeFile != null) {
files.add(relativeFile);
break;
}
} else {
VirtualFile relativeFile = VfsUtil.findRelativeFile(parent, "assets", "vendor", mappingFile.key(), mappingFile.key() + ".index.js");
if (relativeFile != null) {
files.add(relativeFile);
break;
}
}
}
}
} else if (mappingFile.sourceType() == MappingFileEnum.INSTALLED) {
// fallback without project structure: every folder like "vendor/installed.php" => "vendor/bootstrap"
VirtualFile parent = mappingFile.sourceFile().getParent();
if (parent != null) {
if (mappingFile.key().endsWith("css")) {
String[] split = StringUtils.split(mappingFile.key(), "/");
VirtualFile relativeFile = VfsUtil.findRelativeFile(parent, split);
if (relativeFile != null) {
files.add(relativeFile);
break;
}
} else if (mappingFile.key().contains("/")) {
String[] split = StringUtils.split(mappingFile.key() + ".js", "/");
VirtualFile relativeFile = VfsUtil.findRelativeFile(parent, split);
if (relativeFile != null) {
files.add(relativeFile);
break;
}
} else {
VirtualFile relativeFile = VfsUtil.findRelativeFile(parent, mappingFile.key(), mappingFile.key() + ".index.js");
if (relativeFile != null) {
files.add(relativeFile);
break;
}
}
}
}
}

return files;
}

@NotNull
public static Collection<VirtualFile> getModuleReferences(@NotNull Project project, @NotNull String module) {
return getModuleReferences(module, AssetMapperUtil.getMappingFiles(project));
}

@NotNull
public static Collection<VirtualFile> getEntrypointModuleReferences(@NotNull Project project, @NotNull String module) {
List<AssetMapperModule> collect = getEntrypointMappings(project).stream().filter(module1 -> module1.key().equals(module)).collect(Collectors.toList());

// mapping targets
Collection<VirtualFile> files = collect.stream()
.map(AssetMapperModule::sourceFile)
.collect(Collectors.toSet());

// mapping reference tags
files.addAll(getModuleReferences(module, collect));

return files;
}

@NotNull
private static List<AssetMapperModule> getEntrypointMappings(@NotNull Project project) {
return AssetMapperUtil.getMappingFiles(project).stream().filter(m -> m.entrypoint() != null && m.entrypoint()).collect(Collectors.toList());
}

@NotNull
public static Collection<LookupElement> getLookupElements(@NotNull Project project) {
return getLookupElements(AssetMapperUtil.getMappingFiles(project));
}

@NotNull
public static Collection<LookupElement> getEntrypointLookupElements(@NotNull Project project) {
return getLookupElements(getEntrypointMappings(project));
}

@NotNull
public static Collection<LookupElement> getLookupElements(@NotNull List<AssetMapperModule> modules) {
Collection<LookupElement> lookupElements = new ArrayList<>();

Set<String> visited = new HashSet<>();
for (AssetMapperModule module : modules) {
if (visited.contains(module.key())) {
continue;
}

visited.add(module.key());

LookupElementBuilder elementBuilder = LookupElementBuilder.create(module.key()).withIcon(Symfony2Icons.SYMFONY);
String typeText = "";

if (module.url() != null) {
typeText = module.url();
} else if (module.path() != null) {
typeText = module.path();
}

if (module.version() != null) {
if (!typeText.isBlank()) {
typeText = module.version() + " " + typeText;
} else {
typeText = module.version();
}
}

if (!typeText.isBlank()) {
elementBuilder = elementBuilder.withTypeText(typeText);
}

lookupElements.add(elementBuilder);
}

return lookupElements;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package fr.adrienbrault.idea.symfony2plugin.assetMapper.dict;

import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
* @author Daniel Espendiller <[email protected]>
*/
public record AssetMapperModule(
@NotNull MappingFileEnum sourceType,
@NotNull VirtualFile sourceFile,
@NotNull String key,
@Nullable String path,
@Nullable String url,
@Nullable String version,
@Nullable Boolean entrypoint,
@Nullable String type
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package fr.adrienbrault.idea.symfony2plugin.assetMapper.dict;

import org.jetbrains.annotations.NotNull;

/**
* @author Daniel Espendiller <[email protected]>
*/
public enum MappingFileEnum {
IMPORTMAP, INSTALLED;

public static MappingFileEnum fromString(@NotNull String text) {
if (text.equalsIgnoreCase("importmap.php")) {
return IMPORTMAP;
}

if (text.equalsIgnoreCase("installed.php")) {
return INSTALLED;
}

throw new RuntimeException("invalid filename");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package fr.adrienbrault.idea.symfony2plugin.assetMapper.references;

import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.lang.javascript.frameworks.modules.JSResolvableModuleReferenceContributor;
import com.intellij.lang.javascript.psi.resolve.JSResolveResult;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.ResolveResult;
import fr.adrienbrault.idea.symfony2plugin.assetMapper.AssetMapperUtil;
import fr.adrienbrault.idea.symfony2plugin.util.PsiElementUtils;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;

/**
* @author Daniel Espendiller <[email protected]>
*/
public class AssetMapperModuleReferenceContributor extends JSResolvableModuleReferenceContributor {
@Override
protected ResolveResult @NotNull [] resolveElement(@NotNull PsiElement psiElement, @NotNull String module) {
Collection<VirtualFile> files = AssetMapperUtil.getModuleReferences(psiElement.getProject(), module);

if (files.isEmpty()) {
return new ResolveResult[0];
}

return JSResolveResult.toResolveResults(PsiElementUtils.convertVirtualFilesToPsiFiles(psiElement.getProject(), files));
}

@Override
public @NotNull Collection<LookupElement> getLookupElements(@NotNull String unquotedEscapedText, @NotNull PsiElement host) {
return AssetMapperUtil.getLookupElements(host.getProject());
}

@Override
public int getDefaultWeight() {
return 10;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent;
import fr.adrienbrault.idea.symfony2plugin.asset.AssetDirectoryReader;
import fr.adrienbrault.idea.symfony2plugin.asset.provider.AssetCompletionProvider;
import fr.adrienbrault.idea.symfony2plugin.assetMapper.AssetMapperUtil;
import fr.adrienbrault.idea.symfony2plugin.dic.MethodReferenceBag;
import fr.adrienbrault.idea.symfony2plugin.dic.ServiceCompletionProvider;
import fr.adrienbrault.idea.symfony2plugin.routing.RouteHelper;
Expand Down Expand Up @@ -298,6 +299,9 @@ public void addCompletions(@NotNull CompletionParameters parameters, @NotNull Pr
// {% render(controller('<caret>')) %}
extend(CompletionType.BASIC, TwigPattern.getPrintBlockOrTagFunctionPattern("controller"), new ControllerCompletionProvider());

// {{ importmap('<caret>') }}
extend(CompletionType.BASIC, TwigPattern.getPrintBlockOrTagFunctionPattern("importmap"), new ImportmapCompletionProvider());

// {% foo() %}
// {% foo.bar() %}
// {{ 'test'|<caret> }}
Expand Down Expand Up @@ -642,6 +646,17 @@ public void addCompletions(@NotNull CompletionParameters parameters, @NotNull Pr
}
}

private static class ImportmapCompletionProvider extends CompletionProvider<CompletionParameters> {
public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
if(!Symfony2ProjectComponent.isEnabled(position)) {
return;
}

resultSet.addAllElements(AssetMapperUtil.getEntrypointLookupElements(position.getProject()));
}
}

private static class TwigSimpleTestParametersCompletionProvider extends CompletionProvider<CompletionParameters> {
public void addCompletions(@NotNull CompletionParameters parameters, @NotNull ProcessingContext context, @NotNull CompletionResultSet resultSet) {
PsiElement position = parameters.getPosition();
Expand Down
Loading

0 comments on commit 457a4d8

Please sign in to comment.