Skip to content

Commit aef799a

Browse files
committed
Optimize Symfony command inside terminal plugin caching and ensure proper invalidation on PHP modifications and dumb mode handling.
1 parent 6eb3040 commit aef799a

4 files changed

Lines changed: 126 additions & 18 deletions

File tree

src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/integrations/terminal/SymfonyShellCommandSpecsProvider.kt

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package fr.adrienbrault.idea.symfony2plugin.integrations.terminal
22

3-
import com.intellij.openapi.application.ApplicationManager
3+
import com.intellij.openapi.application.ReadAction
4+
import com.intellij.openapi.project.DumbService
45
import com.intellij.openapi.project.Project
6+
import com.intellij.openapi.util.Key
7+
import com.intellij.psi.util.CachedValue
8+
import com.intellij.psi.util.CachedValueProvider
9+
import com.intellij.psi.util.CachedValuesManager
10+
import com.intellij.psi.util.PsiModificationTracker
511
import com.intellij.terminal.completion.spec.ShellRuntimeContext
12+
import com.jetbrains.php.lang.PhpLanguage
613
import fr.adrienbrault.idea.symfony2plugin.Settings
714
import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil
815
import fr.adrienbrault.idea.symfony2plugin.util.SymfonyCommandUtil
@@ -16,6 +23,8 @@ import org.jetbrains.plugins.terminal.block.completion.spec.ShellCommandSpecsPro
1623
import org.jetbrains.plugins.terminal.block.completion.spec.dsl.ShellChildCommandsContext
1724
import org.jetbrains.plugins.terminal.block.completion.spec.project
1825

26+
private val COMMAND_DATA_CACHE = Key.create<CachedValue<List<CommandData>>>("SYMFONY_TERMINAL_COMMAND_DATA")
27+
1928
/**
2029
* Provides terminal completion for Symfony console commands.
2130
*
@@ -90,16 +99,32 @@ private suspend fun ShellChildCommandsContext.addSymfonyCommands(runtimeCtx: She
9099
}
91100
}
92101

93-
internal fun collectCommandData(project: Project): List<CommandData> =
94-
ApplicationManager.getApplication().runReadAction<List<CommandData>> {
95-
SymfonyCommandUtil.getCommands(project).map { command ->
96-
val phpClass = PhpElementsUtil.getClassInterface(project, command.fqn)
97-
CommandData(
98-
name = command.name,
99-
options = if (phpClass != null) SymfonyCommandUtil.getCommandOptions(phpClass) else emptyMap(),
100-
arguments = if (phpClass != null) SymfonyCommandUtil.getCommandArguments(phpClass) else emptyMap(),
101-
)
102-
}
102+
internal fun collectCommandData(project: Project): List<CommandData> {
103+
if (DumbService.isDumb(project)) return emptyList()
104+
105+
return CachedValuesManager.getManager(project).getCachedValue(
106+
project,
107+
COMMAND_DATA_CACHE,
108+
{
109+
ReadAction.nonBlocking<CachedValueProvider.Result<List<CommandData>>> {
110+
CachedValueProvider.Result.create(
111+
collectCommandDataInner(project),
112+
PsiModificationTracker.getInstance(project).forLanguage(PhpLanguage.INSTANCE)
113+
)
114+
}.expireWhen { project.isDisposed }.executeSynchronously()
115+
},
116+
false
117+
)
118+
}
119+
120+
private fun collectCommandDataInner(project: Project): List<CommandData> =
121+
SymfonyCommandUtil.getCommands(project).map { command ->
122+
val phpClass = PhpElementsUtil.getClassInterface(project, command.fqn)
123+
CommandData(
124+
name = command.name,
125+
options = if (phpClass != null) SymfonyCommandUtil.getCommandOptions(phpClass) else emptyMap(),
126+
arguments = if (phpClass != null) SymfonyCommandUtil.getCommandArguments(phpClass) else emptyMap(),
127+
)
103128
}
104129

105130
internal data class CommandData(

src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/runAnything/SymfonyConsoleRunAnythingProvider.kt

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ import com.intellij.ide.actions.runAnything.items.RunAnythingItem
99
import com.intellij.ide.actions.runAnything.items.RunAnythingItemBase
1010
import com.intellij.openapi.actionSystem.CommonDataKeys
1111
import com.intellij.openapi.actionSystem.DataContext
12-
import com.intellij.openapi.application.ApplicationManager
12+
import com.intellij.openapi.application.ReadAction
13+
import com.intellij.openapi.project.DumbService
14+
import com.intellij.openapi.project.Project
15+
import com.intellij.openapi.util.Key
16+
import com.intellij.psi.util.CachedValue
17+
import com.intellij.psi.util.CachedValueProvider
18+
import com.intellij.psi.util.CachedValuesManager
19+
import com.intellij.psi.util.PsiModificationTracker
20+
import com.jetbrains.php.lang.PhpLanguage
1321
import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons
1422
import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent
1523
import fr.adrienbrault.idea.symfony2plugin.dic.command.SymfonyCommandRunConfiguration
@@ -19,6 +27,8 @@ import fr.adrienbrault.idea.symfony2plugin.util.SymfonyCommandUtil
1927
import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyCommand
2028
import javax.swing.Icon
2129

30+
private val COMMAND_CACHE = Key.create<CachedValue<List<SymfonyCommand>>>("SYMFONY_RUN_ANYTHING_COMMANDS")
31+
2232
/**
2333
* Run Anything provider for Symfony console commands.
2434
*
@@ -31,17 +41,29 @@ import javax.swing.Icon
3141
class SymfonyConsoleRunAnythingProvider : RunAnythingProviderBase<SymfonyCommand>() {
3242

3343
override fun getValues(dataContext: DataContext, pattern: String): Collection<SymfonyCommand> {
34-
val project = CommonDataKeys.PROJECT.getData(dataContext)
35-
if (!Symfony2ProjectComponent.isEnabled(project)) return emptyList()
44+
val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return emptyList()
45+
if (!Symfony2ProjectComponent.isEnabled(project) || DumbService.isDumb(project)) return emptyList()
3646

3747
val lowerPattern = pattern.lowercase().trim()
3848

39-
return ApplicationManager.getApplication().runReadAction<Collection<SymfonyCommand>> {
40-
SymfonyCommandUtil.getCommands(project!!)
41-
.filter { lowerPattern in it.name.lowercase() }
42-
}
49+
return getCommands(project).filter { lowerPattern in it.name.lowercase() }
4350
}
4451

52+
private fun getCommands(project: Project): List<SymfonyCommand> =
53+
CachedValuesManager.getManager(project).getCachedValue(
54+
project,
55+
COMMAND_CACHE,
56+
{
57+
ReadAction.nonBlocking<CachedValueProvider.Result<List<SymfonyCommand>>> {
58+
CachedValueProvider.Result.create(
59+
SymfonyCommandUtil.getCommands(project).toList(),
60+
PsiModificationTracker.getInstance(project).forLanguage(PhpLanguage.INSTANCE)
61+
)
62+
}.expireWhen { project.isDisposed }.executeSynchronously()
63+
},
64+
false
65+
)
66+
4567
override fun execute(dataContext: DataContext, value: SymfonyCommand) {
4668
val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return
4769

src/test/kotlin/fr/adrienbrault/idea/symfony2plugin/tests/integrations/terminal/SymfonyShellCommandSpecsProviderTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package fr.adrienbrault.idea.symfony2plugin.tests.integrations.terminal
22

3+
import com.intellij.testFramework.DumbModeTestUtils
34
import fr.adrienbrault.idea.symfony2plugin.integrations.terminal.SymfonyShellCommandSpecsProvider
45
import fr.adrienbrault.idea.symfony2plugin.integrations.terminal.collectCommandData
56
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase
@@ -85,6 +86,37 @@ class SymfonyShellCommandSpecsProviderTest : SymfonyLightCodeInsightFixtureTestC
8586
assertTrue("terminal:modern-options should be collected", "terminal:modern-options" in names)
8687
}
8788

89+
fun testCollectCommandDataInvalidatesOnPhpModification() {
90+
val before = collectCommandData(project).map { it.name }
91+
assertFalse("terminal:after-cache should not exist before adding the PHP file", "terminal:after-cache" in before)
92+
93+
myFixture.addFileToProject(
94+
"src/Command/AfterCacheCommand.php",
95+
"""
96+
<?php
97+
98+
namespace TerminalFixtures;
99+
100+
use Symfony\Component\Console\Attribute\AsCommand;
101+
use Symfony\Component\Console\Command\Command;
102+
103+
#[AsCommand(name: 'terminal:after-cache')]
104+
class AfterCacheCommand extends Command {}
105+
""".trimIndent()
106+
)
107+
108+
val after = collectCommandData(project).map { it.name }
109+
assertTrue("terminal:after-cache should be collected after the PHP modification", "terminal:after-cache" in after)
110+
}
111+
112+
fun testCollectCommandDataReturnsEmptyInDumbMode() {
113+
DumbModeTestUtils.runInDumbModeSynchronously(project) {
114+
val data = collectCommandData(project)
115+
116+
assertTrue(data.isEmpty())
117+
}
118+
}
119+
88120
/**
89121
* Options from `addOption()` calls are reflected with name and shortcut.
90122
*

src/test/kotlin/fr/adrienbrault/idea/symfony2plugin/tests/runAnything/SymfonyConsoleRunAnythingProviderTest.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package fr.adrienbrault.idea.symfony2plugin.tests.runAnything
33
import com.intellij.ide.actions.runAnything.items.RunAnythingItemBase
44
import com.intellij.openapi.actionSystem.CommonDataKeys
55
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
6+
import com.intellij.testFramework.DumbModeTestUtils
67
import fr.adrienbrault.idea.symfony2plugin.runAnything.SymfonyConsoleRunAnythingProvider
78
import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase
89
import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyCommand
@@ -71,6 +72,34 @@ class SymfonyConsoleRunAnythingProviderTest : SymfonyLightCodeInsightFixtureTest
7172
assertTrue(values.any { it.name == "cache:clear" })
7273
}
7374

75+
fun testGetValuesInvalidatesOnPhpModification() {
76+
val before = provider.getValues(createDataContext(), "app:after-cache").map { it.name }
77+
assertFalse("app:after-cache should not exist before adding the PHP file", "app:after-cache" in before)
78+
79+
myFixture.addFileToProject(
80+
"src/Command/AfterCacheCommand.php",
81+
"""
82+
<?php
83+
use Symfony\Component\Console\Attribute\AsCommand;
84+
use Symfony\Component\Console\Command\Command;
85+
86+
#[AsCommand(name: 'app:after-cache')]
87+
class AfterCacheCommand extends Command {}
88+
""".trimIndent()
89+
)
90+
91+
val after = provider.getValues(createDataContext(), "app:after-cache").map { it.name }
92+
assertTrue("app:after-cache should be collected after the PHP modification", "app:after-cache" in after)
93+
}
94+
95+
fun testGetValuesReturnsEmptyInDumbMode() {
96+
DumbModeTestUtils.runInDumbModeSynchronously(project) {
97+
val values = provider.getValues(createDataContext(), "cache")
98+
99+
assertTrue(values.isEmpty())
100+
}
101+
}
102+
74103
// --- getMainListItem ---
75104

76105
fun testGetMainListItemDescriptionIsShortClassName() {

0 commit comments

Comments
 (0)