From c2107afdc2830f0c8b0e32b3dfd102f17ab7c4e3 Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Tue, 4 Feb 2025 17:36:34 +0100 Subject: [PATCH 01/15] Limits table suggestions to project scope (exclude vendor files). --- .../eloquent/table/TableReference.java | 19 ++++--- .../indexes/TableIndex.java | 7 ++- .../codeGeneration/MigrationManager.java | 56 ++++++++++--------- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/eloquent/table/TableReference.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/eloquent/table/TableReference.java index 9047102..3e9401b 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/eloquent/table/TableReference.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/eloquent/table/TableReference.java @@ -10,6 +10,7 @@ import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.indexing.FileBasedIndex; +import com.intellij.util.indexing.IdFilter; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -70,13 +71,17 @@ public TableReference(@NotNull PsiElement element, TextRange rangeInElement) { @Override public Object @NotNull [] getVariants() { List variants = new ArrayList<>(); - Collection tables = FileBasedIndex.getInstance().getAllKeys(TableIndex.INDEX_ID, project); - - for (String table : tables) { - variants.add( - PsiElementUtils.buildSimpleLookupElement(table) - ); - } + FileBasedIndex.getInstance().processAllKeys( + TableIndex.INDEX_ID, + key -> { + if (key instanceof String table) { + variants.add(PsiElementUtils.buildSimpleLookupElement(table)); + } + return true; + }, + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, false) + ); return variants.toArray(); } diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/TableIndex.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/TableIndex.java index 2674141..3f34c70 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/TableIndex.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/TableIndex.java @@ -76,7 +76,7 @@ public class TableIndex extends FileBasedIndexExtension { @Override public int getVersion() { - return 1; + return 2; } @Override @@ -89,6 +89,11 @@ public boolean dependsOnFileContent() { return true; } + @Override + public boolean traceKeyHashToVirtualFileMapping() { + return true; + } + private boolean shouldScanMethod(MethodReference methodReference) { PhpExpression schemaClassReference = methodReference.getClassReference(); diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/support/codeGeneration/MigrationManager.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/support/codeGeneration/MigrationManager.java index f8acd6d..2d00a3c 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/support/codeGeneration/MigrationManager.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/support/codeGeneration/MigrationManager.java @@ -7,17 +7,15 @@ import at.alirezamoh.whisperer_for_laravel.actions.models.dataTables.Table; import at.alirezamoh.whisperer_for_laravel.indexes.TableIndex; import at.alirezamoh.whisperer_for_laravel.support.codeGeneration.vistors.MigrationVisitor; -import at.alirezamoh.whisperer_for_laravel.support.utils.LaravelPaths; -import at.alirezamoh.whisperer_for_laravel.support.utils.MethodUtils; +import at.alirezamoh.whisperer_for_laravel.support.utils.*; import at.alirezamoh.whisperer_for_laravel.support.providers.ModelProvider; -import at.alirezamoh.whisperer_for_laravel.support.utils.PhpClassUtils; -import at.alirezamoh.whisperer_for_laravel.support.utils.StrUtils; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.indexing.FileBasedIndex; +import com.intellij.util.indexing.IdFilter; import com.jetbrains.php.lang.psi.PhpFile; import com.jetbrains.php.lang.psi.elements.PhpClass; import com.jetbrains.php.lang.psi.elements.impl.*; @@ -124,31 +122,35 @@ private void getAllModels(Project project) { * Searches for migration files and extracts tables */ private void searchForMigrations() { - Collection files = new ArrayList<>(); - FileBasedIndex fileBasedIndex = FileBasedIndex.getInstance(); - - Collection tables = fileBasedIndex.getAllKeys(TableIndex.INDEX_ID, project); - - for (String table : tables) { - files.addAll( - fileBasedIndex.getContainingFiles( - TableIndex.INDEX_ID, - table, - GlobalSearchScope.allScope(project) - ) - ); - } - - for (VirtualFile file : files) { - PsiFile psiFile = PsiManager.getInstance(project).findFile(file); - - if (psiFile != null) { - MigrationVisitor migrationVisitor = new MigrationVisitor(); - psiFile.acceptChildren(migrationVisitor); - this.tables.addAll(migrationVisitor.getTables()); + Set keys = new HashSet<>(); + + FileBasedIndex.getInstance().processAllKeys( + TableIndex.INDEX_ID, + key -> { + keys.add(key); + return true; + }, + project + ); + + PsiManager psiManager = PsiManager.getInstance(project); + fileBasedIndex.processFilesContainingAnyKey( + TableIndex.INDEX_ID, + keys, + GlobalSearchScope.allScope(project), + null, + null, + file -> { + PsiFile psiFile = psiManager.findFile(file); + if (psiFile != null) { + MigrationVisitor migrationVisitor = new MigrationVisitor(); + psiFile.acceptChildren(migrationVisitor); + this.tables.addAll(migrationVisitor.getTables()); + } + return true; } - } + ); } /** From 3f39d480ffab29085d4988ef6fe395097434c407 Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Tue, 4 Feb 2025 17:37:43 +0100 Subject: [PATCH 02/15] Limits route name suggestions to project scope. --- .../indexes/RouteIndex.java | 7 ++- .../routing/routeName/RouteReference.java | 56 ++++++++++++------- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/RouteIndex.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/RouteIndex.java index 4d2d04a..d29be27 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/RouteIndex.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/RouteIndex.java @@ -100,7 +100,7 @@ public class RouteIndex extends FileBasedIndexExtension { @Override public int getVersion() { - return 1; + return 2; } @Override @@ -113,6 +113,11 @@ public boolean dependsOnFileContent() { return true; } + @Override + public boolean traceKeyHashToVirtualFileMapping() { + return true; + } + private boolean isLaravelRouteMethod(MethodReference methodReference) { PhpExpression routeClassReference = methodReference.getClassReference(); diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/routing/routeName/RouteReference.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/routing/routeName/RouteReference.java index 3eff361..2e67a95 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/routing/routeName/RouteReference.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/routing/routeName/RouteReference.java @@ -10,13 +10,12 @@ import com.intellij.psi.*; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.indexing.FileBasedIndex; +import com.intellij.util.indexing.IdFilter; import com.jetbrains.php.lang.psi.elements.MethodReference; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.util.*; /** * This class resolves references to route names and provides code completion @@ -57,21 +56,24 @@ public RouteReference(@NotNull PsiElement element, TextRange rangeInElement) { public Object @NotNull [] getVariants() { List variants = new ArrayList<>(); - Collection routes = FileBasedIndex.getInstance().getAllKeys(RouteIndex.INDEX_ID, project); - - for (String route : routes) { - String[] split = route.split(" \\| "); - - if (split.length >= 3) { - variants.add( - LookupElementBuilder - .create(split[1]) - .bold() - .withTypeText(split[0], true) - .withIcon(WhispererForLaravelIcon.LARAVEL_ICON) - ); - } - }; + FileBasedIndex.getInstance().processAllKeys( + RouteIndex.INDEX_ID, + route -> { + String[] split = route.split(" \\| "); + if (split.length >= 3) { + variants.add( + LookupElementBuilder + .create(split[1]) + .bold() + .withTypeText(split[0], true) + .withIcon(WhispererForLaravelIcon.LARAVEL_ICON) + ); + } + return true; + }, + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, false) + ); return variants.toArray(); } @@ -82,14 +84,26 @@ public RouteReference(@NotNull PsiElement element, TextRange rangeInElement) { List results = new ArrayList<>(); FileBasedIndex fileBasedIndex = FileBasedIndex.getInstance(); - Collection keys = fileBasedIndex.getAllKeys(RouteIndex.INDEX_ID, project); + Set matchingKeys = new HashSet<>(); + fileBasedIndex.processAllKeys(RouteIndex.INDEX_ID, key -> { + String[] split = key.split(" \\| "); + if (split.length >= 3 && split[1].equals(routeName)) { + matchingKeys.add(key); + } + return true; + }, + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, false) + ); - for (String key : keys) { + for (String key : matchingKeys) { String[] split = key.split(" \\| "); if (split.length >= 3 && split[1].equals(routeName)) { Collection files = fileBasedIndex.getContainingFiles( - RouteIndex.INDEX_ID, key, GlobalSearchScope.allScope(project) + RouteIndex.INDEX_ID, + key, + GlobalSearchScope.projectScope(project) ); for (VirtualFile file : files) { From b9dff772b83434be34b0fb46f0c341500eb906a8 Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Wed, 5 Feb 2025 17:08:46 +0100 Subject: [PATCH 03/15] Limits serviceProvider searching to project scope. --- .../config/util/ConfigKeyCollector.java | 9 ++++- .../config/util/ConfigKeyResolver.java | 40 ++++++++++++++----- .../config/util/ConfigUtil.java | 2 +- .../indexes/ServiceProviderIndex.java | 7 +++- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyCollector.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyCollector.java index 41973fe..ecd3a06 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyCollector.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyCollector.java @@ -7,6 +7,7 @@ import com.intellij.openapi.project.Project; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.indexing.FileBasedIndex; +import com.intellij.util.indexing.IdFilter; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -70,11 +71,15 @@ private void getVariantsFromServiceProviderIndex(@NotNull Project project, FileB return true; }, - GlobalSearchScope.projectScope(project) + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, true) ); return true; - }, project); + }, + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, true) + ); } private @NotNull String buildKeyValue(String value) { diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyResolver.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyResolver.java index fcebfd6..22ed349 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyResolver.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyResolver.java @@ -12,6 +12,7 @@ import com.intellij.psi.search.GlobalSearchScope; import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.indexing.FileBasedIndex; +import com.intellij.util.indexing.IdFilter; import com.jetbrains.php.lang.psi.elements.ArrayCreationExpression; import com.jetbrains.php.lang.psi.elements.ArrayHashElement; import com.jetbrains.php.lang.psi.elements.PhpPsiElement; @@ -50,11 +51,14 @@ public class ConfigKeyResolver { key, null, (file, serviceProvider) -> checkServiceProviderForConfigKey(text, project, serviceProvider, foundElement), - GlobalSearchScope.projectScope(project) + GlobalSearchScope.allScope(project) ); return foundElement.get() == null; - }, project); + }, + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, true) + ); return foundElement.get(); } @@ -134,6 +138,12 @@ public class ConfigKeyResolver { (file, value) -> { PsiFile psiFile = psiManager.findFile(file); if (psiFile != null) { + String relativePath = getRelativeConfigFilePath(file); + if (relativePath != null && relativePath.equals(text)) { + foundElement.set(psiFile); + return false; + } + String keyPath = extractKeyPath(text, file); if (keyPath != null) { PsiElement element = findConfigKeyInFile(psiFile, keyPath); @@ -152,6 +162,22 @@ public class ConfigKeyResolver { } private @Nullable String extractKeyPath(@NotNull String fullKey, @NotNull VirtualFile configFile) { + String relativePath = getRelativeConfigFilePath(configFile); + if (relativePath == null) { + return null; + } + + String prefixWithDot = relativePath + "."; + if (fullKey.startsWith(prefixWithDot)) { + return fullKey.substring(prefixWithDot.length()); + } else if (fullKey.equals(relativePath)) { + return ""; + } else { + return null; + } + } + + private @Nullable String getRelativeConfigFilePath(@NotNull VirtualFile configFile) { String filePath = configFile.getPath().replace('\\', '/'); int configIndex = filePath.lastIndexOf("/config/"); if (configIndex == -1) { @@ -163,15 +189,7 @@ public class ConfigKeyResolver { relativePath = relativePath.substring(0, relativePath.length() - ".php".length()); } - String configKeyPrefix = relativePath.replace('/', '.'); - String prefixWithDot = configKeyPrefix + "."; - if (fullKey.startsWith(prefixWithDot)) { - return fullKey.substring(prefixWithDot.length()); - } else if (fullKey.equals(configKeyPrefix)) { - return ""; - } else { - return null; - } + return relativePath.replace('/', '.'); } private boolean checkServiceProviderForConfigKey( diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigUtil.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigUtil.java index 87d751e..062d2ea 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigUtil.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigUtil.java @@ -13,7 +13,7 @@ public static void iterateOverFileChildren(String dirName, PsiFile configFile, M ArrayReturnPsiRecursiveVisitor visitor = new ArrayReturnPsiRecursiveVisitor(dirName); configFile.acceptChildren(visitor); - variants.put(dirName, null); + variants.put(dirName, ""); variants.putAll(visitor.getVariants()); } diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/ServiceProviderIndex.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/ServiceProviderIndex.java index 44db78d..95478d2 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/ServiceProviderIndex.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/ServiceProviderIndex.java @@ -77,7 +77,7 @@ public class ServiceProviderIndex extends FileBasedIndexExtension classReferences = extendsList.getReferenceElements(); From 82cfaf0cf7f29d827ce71c017614621a43ad9030 Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Wed, 5 Feb 2025 17:10:41 +0100 Subject: [PATCH 04/15] Limits config key suggestions to project scope. --- .../config/util/ConfigKeyCollector.java | 8 ++++++-- .../config/util/ConfigKeyResolver.java | 5 +++-- .../whisperer_for_laravel/indexes/ConfigIndex.java | 7 ++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyCollector.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyCollector.java index ecd3a06..07b2347 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyCollector.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyCollector.java @@ -40,10 +40,14 @@ private void getKeysFromConfigIndex(@NotNull Project project, FileBasedIndex fil return true; }, - GlobalSearchScope.projectScope(project) + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, false) ); return true; - }, project); + }, + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, false) + ); } private void getVariantsFromServiceProviderIndex(@NotNull Project project, FileBasedIndex fileBasedIndex, List variants) { diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyResolver.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyResolver.java index 22ed349..fa1c79c 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyResolver.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/config/util/ConfigKeyResolver.java @@ -116,7 +116,7 @@ public class ConfigKeyResolver { Collection configFiles = FileBasedIndex.getInstance().getContainingFiles( ConfigIndex.INDEX_ID, text, - GlobalSearchScope.allScope(project) + GlobalSearchScope.projectScope(project) ); for (VirtualFile configFile : configFiles) { @@ -155,7 +155,8 @@ public class ConfigKeyResolver { } return true; }, - GlobalSearchScope.projectScope(project) + GlobalSearchScope.projectScope(project), + IdFilter.getProjectIdFilter(project, false) ); return foundElement.get(); diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/ConfigIndex.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/ConfigIndex.java index b94ff19..eeadf81 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/ConfigIndex.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/indexes/ConfigIndex.java @@ -84,7 +84,7 @@ public String read(@NotNull DataInput dataInput) throws IOException { @Override public int getVersion() { - return 1; + return 2; } @Override @@ -108,6 +108,11 @@ public boolean dependsOnFileContent() { return true; } + @Override + public boolean traceKeyHashToVirtualFileMapping() { + return true; + } + private boolean isConfigFile(VirtualFile file, Project project) { String basePath = project.getBasePath(); if (basePath == null) { From 23794ccc57852e4d91b9acc92e94a7bba7c1d93d Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Wed, 5 Feb 2025 17:51:35 +0100 Subject: [PATCH 05/15] Appends project directory path to the destination path. --- .../models/codeGenerationHelperModels/LaravelDbBuilder.java | 4 ++-- .../codeGenerationHelperModels/LaravelModelGeneration.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/codeGenerationHelperModels/LaravelDbBuilder.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/codeGenerationHelperModels/LaravelDbBuilder.java index df4ea85..5c642da 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/codeGenerationHelperModels/LaravelDbBuilder.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/codeGenerationHelperModels/LaravelDbBuilder.java @@ -16,13 +16,13 @@ public LaravelDbBuilder(List methods, SettingsState settingsState) { this.slug = ""; this.name = "whisperer_for_laravel_base_db_query_builder"; this.extension = ".php"; - this.destination = "/" + ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH; + this.destination = settingsState.getProjectDirectoryPath() + ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH; if (settingsState.isProjectDirectoryEmpty()) { this.filePath = ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH + this.name + ".php"; } else { - this.filePath = ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH + settingsState.getProjectDirectoryPath() + "/" + this.name + ".php"; + this.filePath = settingsState.getProjectDirectoryPath() + ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH + this.name + ".php"; } } diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/codeGenerationHelperModels/LaravelModelGeneration.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/codeGenerationHelperModels/LaravelModelGeneration.java index ac895c1..31cea4e 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/codeGenerationHelperModels/LaravelModelGeneration.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/codeGenerationHelperModels/LaravelModelGeneration.java @@ -18,13 +18,13 @@ public LaravelModelGeneration(String namespace, List laravelModels this.slug = ""; this.name = "whisperer_for_laravel_models_" + shortUuid; this.extension = ".php"; - this.destination = "/" + ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH; + this.destination = settingsState.getProjectDirectoryPath() + ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH; if (settingsState.isProjectDirectoryEmpty()) { this.filePath = ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH + this.name + ".php"; } else { - this.filePath = ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH + settingsState.getProjectDirectoryPath() + "/" + this.name + ".php"; + this.filePath = settingsState.getProjectDirectoryPath() + ProjectDefaultPaths.WHISPERER_FOR_LARAVEL_DIR_PATH + this.name + ".php"; } } From b3c3f50640d736e75669348ded82b1087b5c4d23 Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Wed, 5 Feb 2025 19:19:52 +0100 Subject: [PATCH 06/15] Renames action text. --- src/main/resources/META-INF/plugin.xml | 50 +++++++++++++------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 5017755..f016bf5 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -86,83 +86,83 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From 41ebd5e745b36decf136eb892d827a0409b599a3 Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Wed, 5 Feb 2025 20:32:21 +0100 Subject: [PATCH 07/15] Adds route action go to declaration for older laravel version. --- .../routeAction/RouteActionReference.java | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/routing/routeAction/RouteActionReference.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/routing/routeAction/RouteActionReference.java index 2af0b3b..455abc4 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/routing/routeAction/RouteActionReference.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/routing/routeAction/RouteActionReference.java @@ -5,9 +5,7 @@ import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiDirectory; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReferenceBase; +import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; import com.jetbrains.php.lang.psi.PhpFile; import com.jetbrains.php.lang.psi.elements.Method; @@ -15,15 +13,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * This class resolves references controller actions and provides completion */ -public class RouteActionReference extends PsiReferenceBase { +public class RouteActionReference extends PsiReferenceBase implements PsiPolyVariantReference { /** * The current project */ @@ -52,15 +47,45 @@ public RouteActionReference(@NotNull PsiElement element, TextRange rangeInElemen */ @Override public @Nullable PsiElement resolve() { + return null; + } + + @Override + public ResolveResult @NotNull [] multiResolve(boolean b) { String targetAction = StrUtils.removeQuotes(myElement.getText()); + if (!targetAction.contains("@")) { + return new ResolveResult[0]; + } + for (Map.Entry entry : getAllControllersWithActions().entrySet()) { if (entry.getKey().equals(targetAction)) { - return entry.getValue(); + return new ResolveResult[] { new PsiElementResolveResult(entry.getValue()) }; } } - return null; + String[] parts = targetAction.split("@"); + + if (parts.length != 2) { + return new ResolveResult[0]; + } + + String fullController = parts[0].trim(); + String methodName = parts[1].trim(); + int lastIndex = fullController.lastIndexOf('\\'); + String controllerName = (lastIndex != -1) ? fullController.substring(lastIndex + 1) : fullController; + + Collection controllers = PhpIndexUtils.getPhpClassesByName(controllerName, project); + List foundedVariants = new ArrayList<>(); + for (PhpClass controller : controllers) { + Method method = controller.findMethodByName(methodName); + + if (method != null) { + foundedVariants.add(new PsiElementResolveResult(method)); + } + } + + return foundedVariants.toArray(new ResolveResult[0]); } /** From ac6317059e8d62629d9b40fb6f57c74a7780d97d Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Wed, 5 Feb 2025 22:16:22 +0100 Subject: [PATCH 08/15] Removes unnecessary methods. --- src/main/resources/templates/laravelModels.ftl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/resources/templates/laravelModels.ftl b/src/main/resources/templates/laravelModels.ftl index b982b4a..5df0a17 100644 --- a/src/main/resources/templates/laravelModels.ftl +++ b/src/main/resources/templates/laravelModels.ftl @@ -50,12 +50,9 @@ namespace ${namespace} { * @method static _BaseEloquentCollection|${model.modelName}[] get(array|string $columns = ['*']) * @method static ${model.modelName} getModel() * @method static ${model.modelName}[] getModels(array|string $columns = ['*']) - * @method static _BaseEloquentCollection|${model.modelName}[] hydrate(array $items) * @method static _BaseEloquentCollection|${model.modelName}[] lazy(int $chunkSize = 1000) * @method static _BaseEloquentCollection|${model.modelName}[] lazyById(int $chunkSize = 1000, null|string $column = null, null|string $alias = null) - * @method static _BaseEloquentCollection|${model.modelName}[] lazyByIdDesc(int $chunkSize = 1000, null|string $column = null, null|string $alias = null) * @method static ${model.modelName} make(array $attributes = []) - * @method static ${model.modelName} newModelInstance(array $attributes = []) * @method static _BaseEloquentCollection|LengthAwarePaginator|${model.modelName}[] paginate(\Closure|int|null $perPage = null, array|string $columns = ['*'], string $pageName = 'page', int|null $page = null, \Closure|int|null $total = null) * @method static _BaseEloquentCollection|Paginator|${model.modelName}[] simplePaginate(int|null $perPage = null, array|string $columns = ['*'], string $pageName = 'page', int|null $page = null) * @method static ${model.modelName} sole(array|string $columns = ['*']) From 0ccfc9e37a977c06a3971f282889e06821987548 Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Thu, 6 Feb 2025 23:34:24 +0100 Subject: [PATCH 09/15] Adds inertia page inspection check. --- .../actions/models/InertiaPageModel.java | 55 ++++++ .../inertia/InertiaPageCollector.java | 107 ++++++++++ .../packages/inertia/InertiaReference.java | 101 +--------- .../annotator/CreateInertiaPageIntention.java | 186 ++++++++++++++++++ .../InertiaPageExistenceAnnotator.java | 52 +++++ src/main/resources/META-INF/plugin.xml | 2 + src/main/resources/templates/inertiaPage.ftl | 27 +++ 7 files changed, 431 insertions(+), 99 deletions(-) create mode 100644 src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/InertiaPageModel.java create mode 100644 src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaPageCollector.java create mode 100644 src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/CreateInertiaPageIntention.java create mode 100644 src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/InertiaPageExistenceAnnotator.java create mode 100644 src/main/resources/templates/inertiaPage.ftl diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/InertiaPageModel.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/InertiaPageModel.java new file mode 100644 index 0000000..46a1831 --- /dev/null +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/InertiaPageModel.java @@ -0,0 +1,55 @@ +package at.alirezamoh.whisperer_for_laravel.actions.models; + +import at.alirezamoh.whisperer_for_laravel.settings.SettingsState; + +/** + * Model representing an inertia page + */ +public class InertiaPageModel extends BaseModel { + private String pageName; + + private boolean withOptionsApi; + + /** + * @param name The name of the inertia page + * @param unformattedModuleFullPath The unformatted module full path + * @param formattedModuleFullPath The formatted module full path + */ + public InertiaPageModel( + SettingsState settingsState, + String name, + String unformattedModuleFullPath, + String formattedModuleFullPath, + String defaultDestination, + boolean withOptionsApi, + String pageType + ) + { + super( + settingsState, + name, + unformattedModuleFullPath, + formattedModuleFullPath, + defaultDestination, + "", + pageType, + "" + ); + + this.pageName = getName(); + this.withOptionsApi = withOptionsApi; + } + + @Override + public void setWithoutModuleSrc() { + this.withoutModuleSrcPath = true; + } + + public String getPageName() { + return pageName; + } + + public boolean isWithOptionsApi() { + return withOptionsApi; + } +} diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaPageCollector.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaPageCollector.java new file mode 100644 index 0000000..cb28b82 --- /dev/null +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaPageCollector.java @@ -0,0 +1,107 @@ +package at.alirezamoh.whisperer_for_laravel.packages.inertia; + +import at.alirezamoh.whisperer_for_laravel.settings.SettingsState; +import at.alirezamoh.whisperer_for_laravel.support.utils.DirectoryUtils; +import at.alirezamoh.whisperer_for_laravel.support.utils.StrUtils; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiDirectory; +import com.intellij.psi.PsiFile; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class InertiaPageCollector { + /** + * Collect all Inertia pages based on the paths configured in SettingsState + * + * @param withFile Whether to include the PsiFile reference in the InertiaPage objects. + * @return A list of discovered Inertia pages + */ + public static List collectPages(Project project, boolean withFile) { + List references = new ArrayList<>(); + SettingsState settings = SettingsState.getInstance(project); + + String defaultPath = ""; + if (!settings.isProjectDirectoryEmpty()) { + defaultPath = StrUtils.removeDoubleForwardSlashes( + StrUtils.addSlashes(settings.getProjectDirectoryPath()) + defaultPath + ); + } + + String inertiaPaths = settings.getInertiaPageRootPath(); + if (inertiaPaths == null) { + return references; + } + + String[] paths = inertiaPaths.split(";"); + paths = Arrays.stream(paths) + .filter(path -> !path.isEmpty()) + .toArray(String[]::new); + + for (String path : paths) { + PsiDirectory potentialDir = DirectoryUtils.getDirectory( + project, + StrUtils.removeDoubleForwardSlashes( + defaultPath + + StrUtils.addSlashes(path.replace("\\", "/")) + ) + ); + + if (potentialDir != null) { + references.addAll(getPagesInternal(potentialDir, withFile, "")); + } + } + + return references; + } + + /** + * Recursively collect pages from the given directory + * + * @param dir The directory to scan + * @param withFile Whether to include a PsiFile reference in each InertiaPage + * @param parentPath The accumulated path from root directories + * @return A list of Inertia pages found in this directory and its subdirectories + */ + private static List getPagesInternal(PsiDirectory dir, boolean withFile, String parentPath) { + + List pages = new ArrayList<>(buildPagePath(dir.getFiles(), withFile, parentPath)); + + for (PsiDirectory subDir : dir.getSubdirectories()) { + String newParentPath = parentPath.isEmpty() ? subDir.getName() : parentPath + "/" + subDir.getName(); + pages.addAll(getPagesInternal(subDir, withFile, newParentPath)); + } + + return pages; + } + + /** + * Builds the path for all files in a single directory to detect Vue or JSX files representing Inertia pages. + * + * @param files The array of files in this directory + * @param withFile Whether to include a PsiFile reference in each InertiaPage + * @param parentPath The accumulated parent path segments + * @return A list of Inertia pages + */ + private static List buildPagePath(PsiFile[] files, boolean withFile, String parentPath) { + List pages = new ArrayList<>(); + + for (PsiFile psiFile : files) { + String fileName = psiFile.getName(); + + if (fileName.endsWith(".vue") || fileName.endsWith(".jsx")) { + String pageName = parentPath.isEmpty() ? "" : parentPath + "/"; + pageName += fileName.replaceFirst("\\.(vue|jsx)$", ""); + + if (withFile) { + pages.add(new InertiaPage(pageName, psiFile)); + } else { + pages.add(new InertiaPage(pageName)); + } + } + } + + return pages; + } +} diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaReference.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaReference.java index 9796fb1..44b8a24 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaReference.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaReference.java @@ -1,8 +1,5 @@ package at.alirezamoh.whisperer_for_laravel.packages.inertia; -import at.alirezamoh.whisperer_for_laravel.settings.SettingsState; -import at.alirezamoh.whisperer_for_laravel.support.utils.DirectoryUtils; -import at.alirezamoh.whisperer_for_laravel.support.utils.PluginUtils; import at.alirezamoh.whisperer_for_laravel.support.utils.PsiElementUtils; import at.alirezamoh.whisperer_for_laravel.support.utils.StrUtils; import com.intellij.codeInsight.lookup.LookupElementBuilder; @@ -13,7 +10,6 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; public class InertiaReference extends PsiReferenceBase { @@ -35,7 +31,7 @@ public InertiaReference(@NotNull PsiElement element, TextRange rangeInElement) { @Override public @Nullable PsiElement resolve() { String text = StrUtils.removeQuotes(myElement.getText()); - List pages = collectPages(true); + List pages = InertiaPageCollector.collectPages(project, true); for (InertiaPage page : pages) { if (page.getPath().equals(text)) { @@ -48,7 +44,7 @@ public InertiaReference(@NotNull PsiElement element, TextRange rangeInElement) { @Override public Object @NotNull [] getVariants() { - List pages = collectPages(false); + List pages = InertiaPageCollector.collectPages(project, false); List variants = new ArrayList<>(); for (InertiaPage page : pages) { @@ -59,97 +55,4 @@ public InertiaReference(@NotNull PsiElement element, TextRange rangeInElement) { return variants.toArray(); } - - /** - * Collect all Inertia pages based on the paths configured in SettingsState - * - * @param withFile Whether to include the PsiFile reference in the InertiaPage objects. - * @return A list of discovered Inertia pages - */ - public List collectPages(boolean withFile) { - List references = new ArrayList<>(); - SettingsState settings = SettingsState.getInstance(project); - - String defaultPath = ""; - if (!settings.isProjectDirectoryEmpty()) { - defaultPath = StrUtils.removeDoubleForwardSlashes( - StrUtils.addSlashes(settings.getProjectDirectoryPath()) + defaultPath - ); - } - - String inertiaPaths = settings.getInertiaPageRootPath(); - if (inertiaPaths == null) { - return references; - } - - String[] paths = inertiaPaths.split(";"); - paths = Arrays.stream(paths) - .filter(path -> !path.isEmpty()) - .toArray(String[]::new); - - for (String path : paths) { - PsiDirectory potentialDir = DirectoryUtils.getDirectory( - project, - StrUtils.removeDoubleForwardSlashes( - defaultPath + - StrUtils.addSlashes(path.replace("\\", "/")) - ) - ); - - if (potentialDir != null) { - references.addAll(getPagesInternal(potentialDir, withFile, "")); - } - } - - return references; - } - - /** - * Recursively collect pages from the given directory - * - * @param dir The directory to scan - * @param withFile Whether to include a PsiFile reference in each InertiaPage - * @param parentPath The accumulated path from root directories - * @return A list of Inertia pages found in this directory and its subdirectories - */ - private List getPagesInternal(PsiDirectory dir, boolean withFile, String parentPath) { - - List pages = new ArrayList<>(buildPagePath(dir.getFiles(), withFile, parentPath)); - - for (PsiDirectory subDir : dir.getSubdirectories()) { - String newParentPath = parentPath.isEmpty() ? subDir.getName() : parentPath + "/" + subDir.getName(); - pages.addAll(getPagesInternal(subDir, withFile, newParentPath)); - } - - return pages; - } - - /** - * Builds the path for all files in a single directory to detect Vue or JSX files representing Inertia pages. - * - * @param files The array of files in this directory - * @param withFile Whether to include a PsiFile reference in each InertiaPage - * @param parentPath The accumulated parent path segments - * @return A list of Inertia pages - */ - private List buildPagePath(PsiFile[] files, boolean withFile, String parentPath) { - List pages = new ArrayList<>(); - - for (PsiFile psiFile : files) { - String fileName = psiFile.getName(); - - if (fileName.endsWith(".vue") || fileName.endsWith(".jsx")) { - String pageName = parentPath.isEmpty() ? "" : parentPath + "/"; - pageName += fileName.replaceFirst("\\.(vue|jsx)$", ""); - - if (withFile) { - pages.add(new InertiaPage(pageName, psiFile)); - } else { - pages.add(new InertiaPage(pageName)); - } - } - } - - return pages; - } } \ No newline at end of file diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/CreateInertiaPageIntention.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/CreateInertiaPageIntention.java new file mode 100644 index 0000000..56cf6dc --- /dev/null +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/CreateInertiaPageIntention.java @@ -0,0 +1,186 @@ +package at.alirezamoh.whisperer_for_laravel.packages.inertia.annotator; + +import at.alirezamoh.whisperer_for_laravel.actions.models.InertiaPageModel; +import at.alirezamoh.whisperer_for_laravel.settings.SettingsState; +import at.alirezamoh.whisperer_for_laravel.support.TemplateLoader; +import at.alirezamoh.whisperer_for_laravel.support.utils.StrUtils; +import com.intellij.codeInsight.intention.impl.BaseIntentionAction; +import com.intellij.codeInspection.util.IntentionFamilyName; +import com.intellij.codeInspection.util.IntentionName; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.popup.IconButton; +import com.intellij.openapi.ui.popup.JBPopup; +import com.intellij.openapi.ui.popup.JBPopupFactory; +import com.intellij.psi.PsiFile; +import com.intellij.ui.JBColor; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Intention action to create a new Inertia page + */ +public class CreateInertiaPageIntention extends BaseIntentionAction { + /** + * The raw page path where the new page will be created + */ + private final String pagePath; + + /** + * The project settings + */ + private SettingsState settingsState; + + /** + * @param pagePath The target page path + */ + public CreateInertiaPageIntention(String pagePath) { + this.pagePath = StrUtils.removeQuotes(pagePath); + } + + + /** + * Determines if this intention action is available + * In this case, the action is always available + * + * @param project The current project + * @param editor The editor instance + * @param psiFile The current PSI file + * @return true or false + */ + @Override + public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiFile) { + return true; + } + + /** + * Execute the intention action + * + * @param project The current project. + * @param editor The editor instance. + * @param psiFile The current PSI file. + * @throws IncorrectOperationException If the operation cannot be completed + */ + @Override + public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) throws IncorrectOperationException { + List options = getInertiaPaths(project); + if (options == null) { + return; + } + + JPanel contentPanel = new JPanel(new BorderLayout()); + contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + JPanel comboPanel = new JPanel(); + comboPanel.setLayout(new BoxLayout(comboPanel, BoxLayout.Y_AXIS)); + + ComboBox pathsComboBox = new ComboBox<>(options.toArray(new String[0])); + pathsComboBox.setAlignmentX(Component.LEFT_ALIGNMENT); + + ComboBox pageVariantComboBox = new ComboBox<>(new String[]{"Options API", "Composition API"}); + pageVariantComboBox.setAlignmentX(Component.LEFT_ALIGNMENT); + + ComboBox pageTypeComboBox = new ComboBox<>(new String[]{".vue", ".jsx"}); + pageTypeComboBox.setAlignmentX(Component.LEFT_ALIGNMENT); + + + JLabel destinationLabel = new JLabel("Destination directory:"); + destinationLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + comboPanel.add(destinationLabel); + comboPanel.add(pathsComboBox); + comboPanel.add(Box.createVerticalStrut(10)); + + JLabel pageTypeLabel = new JLabel("Page type:"); + pageTypeLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + comboPanel.add(pageTypeLabel); + comboPanel.add(pageTypeComboBox); + comboPanel.add(Box.createVerticalStrut(10)); + + JLabel pageVariantLabel = new JLabel("Page variant:"); + pageVariantLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + comboPanel.add(pageVariantLabel); + comboPanel.add(pageVariantComboBox); + + contentPanel.add(comboPanel, BorderLayout.CENTER); + + JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton createButton = new JButton("Create"); + JButton cancelButton = new JButton("Cancel"); + createButton.setForeground(JBColor.blue); + cancelButton.setForeground(JBColor.gray); + buttonsPanel.add(createButton); + buttonsPanel.add(cancelButton); + contentPanel.add(buttonsPanel, BorderLayout.SOUTH); + + JBPopup popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder(contentPanel, comboPanel) + .setTitle("Create Inertia Page") + .setResizable(false) + .setMovable(true) + .setRequestFocus(true) + .setCancelButton(new IconButton(null, null)) + .createPopup(); + + createButton.addActionListener(e -> { + TemplateLoader templateProcessor = new TemplateLoader( + project, + "inertiaPage.ftl", + new InertiaPageModel( + SettingsState.getInstance(project), + pagePath, + "", + "", + pathsComboBox.getItem(), + pageVariantComboBox.getItem().equals("Options API"), + pageTypeComboBox.getItem() + ) + ); + + WriteCommandAction.runWriteCommandAction(project, () -> { + templateProcessor.createTemplateWithDirectory(true); + }); + + popup.closeOk(null); + }); + cancelButton.addActionListener(e -> popup.cancel()); + + popup.showInBestPositionFor(editor); + } + + /** + * Retrieves the available Inertia paths from the project settings + * + * @param project The current project + * @return A list of inertia paths or an empty list if none are set + */ + private List getInertiaPaths(@NotNull Project project) { + settingsState = SettingsState.getInstance(project); + String inertiaPaths = settingsState.getInertiaPageRootPath(); + if (inertiaPaths == null) { + return new ArrayList<>(); + } + + String[] paths = inertiaPaths.split(";"); + return Arrays.stream(paths) + .filter(path -> !path.isEmpty()) + .toList(); + } + + @Override + public @NotNull @IntentionFamilyName String getFamilyName() { + return "Create inertia page"; + } + + @Override + public @NotNull @IntentionName String getText() { + return "Create inertia page"; + } +} diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/InertiaPageExistenceAnnotator.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/InertiaPageExistenceAnnotator.java new file mode 100644 index 0000000..0048f2b --- /dev/null +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/InertiaPageExistenceAnnotator.java @@ -0,0 +1,52 @@ +package at.alirezamoh.whisperer_for_laravel.packages.inertia.annotator; + +import at.alirezamoh.whisperer_for_laravel.packages.inertia.InertiaPageCollector; +import at.alirezamoh.whisperer_for_laravel.support.utils.StrUtils; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.Annotator; +import com.intellij.lang.annotation.HighlightSeverity; +import com.intellij.psi.PsiElement; +import com.jetbrains.php.lang.psi.elements.MethodReference; +import com.jetbrains.php.lang.psi.elements.StringLiteralExpression; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/** + * Annotator that checks if an Inertia page exists + *

+ * This annotator looks for an inertia page path within a PHP method call to "render" + * It then checks whether the referenced page (based on its path) exists in the project + * If the page is not found, it creates a warning annotation and provides a quick fix + * to create the missing Inertia page + */ +public class InertiaPageExistenceAnnotator implements Annotator { + /** + * Inspects the given PSI element and adds a warning annotation if an Inertia page is missing + * + * @param psiElement the PSI element to inspect + * @param annotationHolder the holder to register annotations + */ + @Override + public void annotate(@NotNull PsiElement psiElement, @NotNull AnnotationHolder annotationHolder) { + if (psiElement == null) { + return; + } + + if (psiElement instanceof StringLiteralExpression stringLiteralExpression) { + PsiElement parent = stringLiteralExpression.getParent().getParent(); + + if (parent instanceof MethodReference methodReference && Objects.equals(methodReference.getName(), "render")) { + boolean exists = InertiaPageCollector.collectPages(psiElement.getProject(), false) + .stream() + .anyMatch(page -> page.getPath().equals(StrUtils.removeQuotes(stringLiteralExpression.getText()))); + if (!exists) { + annotationHolder.newAnnotation(HighlightSeverity.WARNING, "Inertia page not found") + .range(stringLiteralExpression.getTextRange()) + .withFix(new CreateInertiaPageIntention(stringLiteralExpression.getText())) + .create(); + } + } + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index f016bf5..6074d93 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -80,6 +80,8 @@ + + diff --git a/src/main/resources/templates/inertiaPage.ftl b/src/main/resources/templates/inertiaPage.ftl new file mode 100644 index 0000000..819aa17 --- /dev/null +++ b/src/main/resources/templates/inertiaPage.ftl @@ -0,0 +1,27 @@ +<#if withOptionsApi> + + + + + +<#else> + + + + + + From 646b322adee77cf63f8077890962a686f65a083b Mon Sep 17 00:00:00 2001 From: Alireza Mohammadi Date: Thu, 6 Feb 2025 23:46:30 +0100 Subject: [PATCH 10/15] Adds inertia page creation action. --- .../actions/InertiaPageAction.java | 23 +++ .../actions/models/InertiaPageModel.java | 7 + .../actions/views/InertiaPageView.java | 137 ++++++++++++++++++ .../inertia/InertiaPageCollector.java | 20 +++ .../annotator/CreateInertiaPageIntention.java | 26 +--- src/main/resources/META-INF/plugin.xml | 3 + src/main/resources/templates/inertiaPage.ftl | 14 ++ 7 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 src/main/java/at/alirezamoh/whisperer_for_laravel/actions/InertiaPageAction.java create mode 100644 src/main/java/at/alirezamoh/whisperer_for_laravel/actions/views/InertiaPageView.java diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/InertiaPageAction.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/InertiaPageAction.java new file mode 100644 index 0000000..35d585c --- /dev/null +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/InertiaPageAction.java @@ -0,0 +1,23 @@ +package at.alirezamoh.whisperer_for_laravel.actions; + +import at.alirezamoh.whisperer_for_laravel.actions.views.InertiaPageView; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +public class InertiaPageAction extends BaseAction { + @Override + public void actionPerformed(@NotNull AnActionEvent anActionEvent) { + Project project = anActionEvent.getProject(); + InertiaPageView inertiaPageView = new InertiaPageView(project); + + if (inertiaPageView.showAndGet()) { + this.create( + inertiaPageView.getInertiaPageModel(), + "inertiaPage.ftl", + true, + project + ); + } + } +} diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/InertiaPageModel.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/InertiaPageModel.java index 46a1831..1261ce2 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/InertiaPageModel.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/models/InertiaPageModel.java @@ -10,6 +10,8 @@ public class InertiaPageModel extends BaseModel { private boolean withOptionsApi; + private boolean vue; + /** * @param name The name of the inertia page * @param unformattedModuleFullPath The unformatted module full path @@ -38,6 +40,7 @@ public InertiaPageModel( this.pageName = getName(); this.withOptionsApi = withOptionsApi; + this.vue = pageType.equals(".vue"); } @Override @@ -52,4 +55,8 @@ public String getPageName() { public boolean isWithOptionsApi() { return withOptionsApi; } + + public boolean isVue() { + return vue; + } } diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/views/InertiaPageView.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/views/InertiaPageView.java new file mode 100644 index 0000000..a09db7b --- /dev/null +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/actions/views/InertiaPageView.java @@ -0,0 +1,137 @@ +package at.alirezamoh.whisperer_for_laravel.actions.views; + +import at.alirezamoh.whisperer_for_laravel.actions.models.InertiaPageModel; +import at.alirezamoh.whisperer_for_laravel.packages.inertia.InertiaPageCollector; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.ComboBox; +import com.intellij.openapi.ui.ValidationInfo; +import com.intellij.util.ui.JBUI; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.util.Objects; + +public class InertiaPageView extends BaseDialog { + /** + * Text field for entering the inertia page name + */ + private JTextField inertiaPageNameTextField; + + /** + * Select the destination path + */ + protected ComboBox resourcePageDirectoryComboBox; + + /** + * Select the page variant + */ + protected ComboBox pageVariantComboBox; + + /** + * Select the page type + */ + protected ComboBox pageTypeComboBox; + + /** + * @param project The current project + */ + public InertiaPageView(Project project) { + super(project); + + setTitle("Create Inertia Page"); + setSize(500, 200); + setResizable(false); + init(); + } + + /** + * Returns the Inertia page model representing an inertia page + * @return The inertia page model + */ + public InertiaPageModel getInertiaPageModel() { + return new InertiaPageModel( + projectSettingState, + inertiaPageNameTextField.getText(), + "", + "", + resourcePageDirectoryComboBox.getItem(), + Objects.equals(pageVariantComboBox.getItem(), "Options API"), + pageTypeComboBox.getItem() + ); + } + + /** + * Returns the focused component when the dialog opens + * @return The preferred focused component + */ + @Override + public @Nullable JComponent getPreferredFocusedComponent() { + return inertiaPageNameTextField; + } + + /** + * Validates the dialog input + * @return Validation info if there are errors, null otherwise + */ + @Override + protected @Nullable ValidationInfo doValidate() { + String text = inertiaPageNameTextField.getText().trim(); + + if (text.isEmpty()) { + return new ValidationInfo("", inertiaPageNameTextField); + } + return null; + } + + /** + * Creates the center panel of the dialog + * @return The center panel + */ + @Override + protected JComponent createCenterPanel() { + contentPane = new JPanel(); + contentPane.setLayout(new GridBagLayout()); + + inertiaPageNameTextField = new JTextField(); + resourcePageDirectoryComboBox = new ComboBox<>(InertiaPageCollector.getInertiaPaths(project).toArray(new String[0])); + pageVariantComboBox = new ComboBox<>(new String[]{"Options API", "Composition API"}); + pageTypeComboBox = new ComboBox<>(new String[]{".vue", ".jsx"}); + + initDefaultContentPaneSettings(); + + gbc.insets = JBUI.insetsLeft(3); + contentPane.add(new JLabel("Enter page name:"), gbc); + gbc.insets = JBUI.insetsLeft(0); + gbc.insets = JBUI.insetsBottom(15); + gbc.gridy++; + contentPane.add(inertiaPageNameTextField, gbc); + inertiaPageNameTextField.requestFocusInWindow(); + + gbc.gridy++; + gbc.insets = JBUI.insetsLeft(3); + contentPane.add(new JLabel("Page type:"), gbc); + gbc.insets = JBUI.insetsLeft(0); + gbc.insets = JBUI.insetsBottom(15); + gbc.gridy++; + contentPane.add(pageTypeComboBox, gbc); + + gbc.gridy++; + gbc.insets = JBUI.insetsLeft(3); + contentPane.add(new JLabel("Page variant: (Ignore if it's not a Vue file)"), gbc); + gbc.insets = JBUI.insetsLeft(0); + gbc.insets = JBUI.insetsBottom(15); + gbc.gridy++; + contentPane.add(pageVariantComboBox, gbc); + + gbc.gridy++; + this.gbc.insets = JBUI.insetsLeft(3); + this.contentPane.add(new JLabel("Page resource directory:"), this.gbc); + this.gbc.gridy++; + this.gbc.insets = JBUI.insetsLeft(0); + this.gbc.insets = JBUI.insetsBottom(15); + this.contentPane.add(resourcePageDirectoryComboBox, this.gbc); + + return contentPane; + } +} diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaPageCollector.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaPageCollector.java index cb28b82..7b4514d 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaPageCollector.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/InertiaPageCollector.java @@ -6,6 +6,7 @@ import com.intellij.openapi.project.Project; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Arrays; @@ -56,6 +57,25 @@ public static List collectPages(Project project, boolean withFile) return references; } + /** + * Retrieves the available Inertia paths from the project settings + * + * @param project The current project + * @return A list of inertia paths or an empty list if none are set + */ + public static List getInertiaPaths(@NotNull Project project) { + SettingsState settingsState = SettingsState.getInstance(project); + String inertiaPaths = settingsState.getInertiaPageRootPath(); + if (inertiaPaths == null) { + return new ArrayList<>(); + } + + String[] paths = inertiaPaths.split(";"); + return Arrays.stream(paths) + .filter(path -> !path.isEmpty()) + .toList(); + } + /** * Recursively collect pages from the given directory * diff --git a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/CreateInertiaPageIntention.java b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/CreateInertiaPageIntention.java index 56cf6dc..7d5e3ed 100644 --- a/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/CreateInertiaPageIntention.java +++ b/src/main/java/at/alirezamoh/whisperer_for_laravel/packages/inertia/annotator/CreateInertiaPageIntention.java @@ -1,6 +1,7 @@ package at.alirezamoh.whisperer_for_laravel.packages.inertia.annotator; import at.alirezamoh.whisperer_for_laravel.actions.models.InertiaPageModel; +import at.alirezamoh.whisperer_for_laravel.packages.inertia.InertiaPageCollector; import at.alirezamoh.whisperer_for_laravel.settings.SettingsState; import at.alirezamoh.whisperer_for_laravel.support.TemplateLoader; import at.alirezamoh.whisperer_for_laravel.support.utils.StrUtils; @@ -21,8 +22,6 @@ import javax.swing.*; import java.awt.*; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; /** @@ -71,7 +70,7 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiF */ @Override public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) throws IncorrectOperationException { - List options = getInertiaPaths(project); + List options = InertiaPageCollector.getInertiaPaths(project); if (options == null) { return; } @@ -104,7 +103,7 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) thr comboPanel.add(pageTypeComboBox); comboPanel.add(Box.createVerticalStrut(10)); - JLabel pageVariantLabel = new JLabel("Page variant:"); + JLabel pageVariantLabel = new JLabel("Page variant: (Ignore if it's not a Vue file)"); pageVariantLabel.setAlignmentX(Component.LEFT_ALIGNMENT); comboPanel.add(pageVariantLabel); comboPanel.add(pageVariantComboBox); @@ -155,25 +154,6 @@ public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) thr popup.showInBestPositionFor(editor); } - /** - * Retrieves the available Inertia paths from the project settings - * - * @param project The current project - * @return A list of inertia paths or an empty list if none are set - */ - private List getInertiaPaths(@NotNull Project project) { - settingsState = SettingsState.getInstance(project); - String inertiaPaths = settingsState.getInertiaPageRootPath(); - if (inertiaPaths == null) { - return new ArrayList<>(); - } - - String[] paths = inertiaPaths.split(";"); - return Arrays.stream(paths) - .filter(path -> !path.isEmpty()) - .toList(); - } - @Override public @NotNull @IntentionFamilyName String getFamilyName() { return "Create inertia page"; diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 6074d93..7259818 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -159,6 +159,9 @@ + + + diff --git a/src/main/resources/templates/inertiaPage.ftl b/src/main/resources/templates/inertiaPage.ftl index 819aa17..d7ddf53 100644 --- a/src/main/resources/templates/inertiaPage.ftl +++ b/src/main/resources/templates/inertiaPage.ftl @@ -1,3 +1,4 @@ +<#if vue> <#if withOptionsApi>