Skip to content

Commit c0c3287

Browse files
committed
Add file icon provider for Symfony commands
1 parent 23f9bda commit c0c3287

6 files changed

Lines changed: 168 additions & 1 deletion

File tree

src/main/java/fr/adrienbrault/idea/symfony2plugin/Symfony2Icons.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ public class Symfony2Icons {
7979
public static final Icon SYMFONY_AI = IconLoader.getIcon("/icons/symfony_ai.png", Symfony2Icons.class);
8080
public static final Icon SYMFONY_AI_OPACITY = IconLoader.getIcon("/icons/symfony_ai_opacity.png", Symfony2Icons.class);
8181
public static final Icon SYMFONY_ATTRIBUTE = IconLoader.getIcon("/icons/symfony_attribute.svg", Symfony2Icons.class);
82+
public static final Icon SYMFONY_COMMAND_FILE = IconLoader.getIcon("/icons/symfony_command_file.svg", Symfony2Icons.class);
8283

8384
public static Image getImage(Icon icon) {
8485

@@ -94,4 +95,3 @@ public static Image getImage(Icon icon) {
9495
return image;
9596
}
9697
}
97-
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package fr.adrienbrault.idea.symfony2plugin.dic.command
2+
3+
import com.intellij.ide.FileIconProvider
4+
import com.intellij.openapi.fileTypes.FileTypeRegistry
5+
import com.intellij.openapi.project.DumbService
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.util.text.StringUtil
8+
import com.intellij.openapi.vfs.VirtualFile
9+
import com.intellij.psi.PsiManager
10+
import com.intellij.psi.impl.ElementBase
11+
import com.intellij.ui.IconManager
12+
import com.intellij.ui.LayeredIcon
13+
import com.jetbrains.php.lang.PhpFileType
14+
import com.jetbrains.php.lang.psi.PhpFile
15+
import com.jetbrains.php.lang.psi.elements.PhpClass
16+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons
17+
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent
18+
import javax.swing.Icon
19+
import javax.swing.SwingConstants
20+
21+
private const val AS_COMMAND_ATTRIBUTE = "\\Symfony\\Component\\Console\\Attribute\\AsCommand"
22+
23+
/**
24+
* WIP: decorate Symfony attribute command PHP file icons while keeping PHP's own class icon as the base.
25+
*/
26+
class SymfonyCommandFileIconProvider : FileIconProvider {
27+
28+
override fun getIcon(virtualFile: VirtualFile, flags: Int, project: Project?): Icon? {
29+
if (project == null ||
30+
!Symfony2ProjectComponent.isEnabled(project) ||
31+
DumbService.getInstance(project).isDumb ||
32+
!FileTypeRegistry.getInstance().isFileOfType(virtualFile, PhpFileType.INSTANCE)
33+
) {
34+
return null
35+
}
36+
37+
val psiFile = PsiManager.getInstance(project).findFile(virtualFile) as? PhpFile ?: return null
38+
val phpClass = extractOnlyClass(psiFile) ?: return null
39+
if (phpClass.getAttributes(AS_COMMAND_ATTRIBUTE).isEmpty()) {
40+
return null
41+
}
42+
43+
val icon = createSymfonyCommandFileIcon(phpClass.icon)
44+
return IconManager.getInstance().createLayeredIcon(psiFile, icon, ElementBase.transformFlags(psiFile, flags))
45+
}
46+
47+
private fun extractOnlyClass(file: PhpFile): PhpClass? {
48+
val classes = file.topLevelDefs.values().filterIsInstance<PhpClass>()
49+
if (classes.size != 1) {
50+
return null
51+
}
52+
53+
val phpClass = classes.first()
54+
return if (StringUtil.containsIgnoreCase(file.name, phpClass.name)) phpClass else null
55+
}
56+
57+
}
58+
59+
internal fun createSymfonyCommandFileIcon(baseIcon: Icon): Icon {
60+
val badgeIcon = Symfony2Icons.SYMFONY_COMMAND_FILE
61+
return LayeredIcon.layeredIcon(arrayOf(baseIcon, badgeIcon)).also {
62+
it.setIcon(badgeIcon, 1, SwingConstants.SOUTH_EAST)
63+
}
64+
}

src/main/resources/META-INF/plugin.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,8 @@
341341
<codeInsight.inlayProvider language="Twig" implementationClass="fr.adrienbrault.idea.symfony2plugin.templating.inlay.TwigVariablesInlayHintsProvider"/>
342342

343343
<multiHostInjector implementation="fr.adrienbrault.idea.symfony2plugin.lang.ParameterLanguageInjector"/>
344+
<fileIconProvider implementation="fr.adrienbrault.idea.symfony2plugin.dic.command.SymfonyCommandFileIconProvider"
345+
order="before PhpFileIconProvider"/>
344346
<iconProvider implementation="fr.adrienbrault.idea.symfony2plugin.twig.icon.TwigIconProvider"/>
345347

346348
<statusBarWidgetFactory implementation="fr.adrienbrault.idea.symfony2plugin.ui.SymfonyStatusbarWidgetFactory" id="symfony.status.bar"/>
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 5 additions & 0 deletions
Loading
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package fr.adrienbrault.idea.symfony2plugin.tests.dic.command
2+
3+
import com.intellij.psi.PsiDocumentManager
4+
import com.intellij.psi.PsiFile
5+
import com.intellij.ui.LayeredIcon
6+
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons
7+
import fr.adrienbrault.idea.symfony2plugin.dic.command.SymfonyCommandFileIconProvider
8+
import fr.adrienbrault.idea.symfony2plugin.dic.command.createSymfonyCommandFileIcon
9+
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase
10+
import javax.swing.Icon
11+
12+
/**
13+
* @see SymfonyCommandFileIconProvider
14+
*/
15+
class SymfonyCommandFileIconProviderTest : SymfonyLightCodeInsightFixtureTestCase() {
16+
17+
fun testAsCommandAttributeShowsSymfonyBadge() {
18+
val commandFile = myFixture.addFileToProject(
19+
"src/Command/FooCommand.php",
20+
"""
21+
<?php
22+
23+
namespace App\Command;
24+
25+
use Symfony\Component\Console\Attribute\AsCommand;
26+
27+
#[AsCommand(name: 'app:foo')]
28+
class FooCommand
29+
{
30+
}
31+
""".trimIndent()
32+
)
33+
34+
val icon = getIconFromProvider(commandFile)
35+
assertNotNull("Icon should not be null for AsCommand class", icon)
36+
}
37+
38+
fun testSymfonyCommandBadgeIconIsLayered() {
39+
val icon: Icon = createSymfonyCommandFileIcon(Symfony2Icons.SYMFONY)
40+
41+
assertTrue("Badge icon should be a LayeredIcon", icon is LayeredIcon)
42+
assertTrue("Command badge should be loaded", Symfony2Icons.SYMFONY_COMMAND_FILE.iconWidth > 0)
43+
assertTrue("Badge icon should keep a visible size", icon.iconWidth > 0 && icon.iconHeight > 0)
44+
}
45+
46+
fun testRegularPhpClassFallsBackToPhpProvider() {
47+
val regularFile = myFixture.addFileToProject(
48+
"src/Command/FooService.php",
49+
"""
50+
<?php
51+
52+
namespace App\Command;
53+
54+
class FooService
55+
{
56+
}
57+
""".trimIndent()
58+
)
59+
60+
assertNull("Provider should not handle regular PHP classes", getIconFromProvider(regularFile))
61+
}
62+
63+
fun testMultipleClassesFallBackToPhpProvider() {
64+
val multipleClassesFile = myFixture.addFileToProject(
65+
"src/Command/FooCommand.php",
66+
"""
67+
<?php
68+
69+
namespace App\Command;
70+
71+
use Symfony\Component\Console\Attribute\AsCommand;
72+
73+
#[AsCommand(name: 'app:foo')]
74+
class FooCommand
75+
{
76+
}
77+
78+
class Helper
79+
{
80+
}
81+
""".trimIndent()
82+
)
83+
84+
assertNull("Provider should not handle PHP files with multiple classes", getIconFromProvider(multipleClassesFile))
85+
}
86+
87+
private fun getIconFromProvider(psiFile: PsiFile): Icon? {
88+
PsiDocumentManager.getInstance(project).commitAllDocuments()
89+
return SymfonyCommandFileIconProvider().getIcon(psiFile.virtualFile!!, 0, project)
90+
}
91+
}

0 commit comments

Comments
 (0)