Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for AssetMapper and importmap #2254

Merged
merged 1 commit into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading