diff --git a/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/integrations/terminal/SymfonyShellCommandSpecsProvider.kt b/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/integrations/terminal/SymfonyShellCommandSpecsProvider.kt index dc3633376..487d22497 100644 --- a/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/integrations/terminal/SymfonyShellCommandSpecsProvider.kt +++ b/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/integrations/terminal/SymfonyShellCommandSpecsProvider.kt @@ -1,8 +1,15 @@ package fr.adrienbrault.idea.symfony2plugin.integrations.terminal -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.psi.util.CachedValue +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.PsiModificationTracker import com.intellij.terminal.completion.spec.ShellRuntimeContext +import com.jetbrains.php.lang.PhpLanguage import fr.adrienbrault.idea.symfony2plugin.Settings import fr.adrienbrault.idea.symfony2plugin.util.PhpElementsUtil import fr.adrienbrault.idea.symfony2plugin.util.SymfonyCommandUtil @@ -16,6 +23,8 @@ import org.jetbrains.plugins.terminal.block.completion.spec.ShellCommandSpecsPro import org.jetbrains.plugins.terminal.block.completion.spec.dsl.ShellChildCommandsContext import org.jetbrains.plugins.terminal.block.completion.spec.project +private val COMMAND_DATA_CACHE = Key.create>>("SYMFONY_TERMINAL_COMMAND_DATA") + /** * Provides terminal completion for Symfony console commands. * @@ -90,16 +99,32 @@ private suspend fun ShellChildCommandsContext.addSymfonyCommands(runtimeCtx: She } } -internal fun collectCommandData(project: Project): List = - ApplicationManager.getApplication().runReadAction> { - SymfonyCommandUtil.getCommands(project).map { command -> - val phpClass = PhpElementsUtil.getClassInterface(project, command.fqn) - CommandData( - name = command.name, - options = if (phpClass != null) SymfonyCommandUtil.getCommandOptions(phpClass) else emptyMap(), - arguments = if (phpClass != null) SymfonyCommandUtil.getCommandArguments(phpClass) else emptyMap(), - ) - } +internal fun collectCommandData(project: Project): List { + if (DumbService.isDumb(project)) return emptyList() + + return CachedValuesManager.getManager(project).getCachedValue( + project, + COMMAND_DATA_CACHE, + { + ReadAction.nonBlocking>> { + CachedValueProvider.Result.create( + collectCommandDataInner(project), + PsiModificationTracker.getInstance(project).forLanguage(PhpLanguage.INSTANCE) + ) + }.expireWhen { project.isDisposed }.executeSynchronously() + }, + false + ) +} + +private fun collectCommandDataInner(project: Project): List = + SymfonyCommandUtil.getCommands(project).map { command -> + val phpClass = PhpElementsUtil.getClassInterface(project, command.fqn) + CommandData( + name = command.name, + options = if (phpClass != null) SymfonyCommandUtil.getCommandOptions(phpClass) else emptyMap(), + arguments = if (phpClass != null) SymfonyCommandUtil.getCommandArguments(phpClass) else emptyMap(), + ) } internal data class CommandData( diff --git a/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/runAnything/SymfonyConsoleRunAnythingProvider.kt b/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/runAnything/SymfonyConsoleRunAnythingProvider.kt index 8b71f635e..86aeacaf0 100644 --- a/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/runAnything/SymfonyConsoleRunAnythingProvider.kt +++ b/src/main/kotlin/fr/adrienbrault/idea/symfony2plugin/runAnything/SymfonyConsoleRunAnythingProvider.kt @@ -9,7 +9,15 @@ import com.intellij.ide.actions.runAnything.items.RunAnythingItem import com.intellij.ide.actions.runAnything.items.RunAnythingItemBase import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.psi.util.CachedValue +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.PsiModificationTracker +import com.jetbrains.php.lang.PhpLanguage import fr.adrienbrault.idea.symfony2plugin.Symfony2Icons import fr.adrienbrault.idea.symfony2plugin.Symfony2ProjectComponent import fr.adrienbrault.idea.symfony2plugin.dic.command.SymfonyCommandRunConfiguration @@ -19,6 +27,8 @@ import fr.adrienbrault.idea.symfony2plugin.util.SymfonyCommandUtil import fr.adrienbrault.idea.symfony2plugin.util.dict.SymfonyCommand import javax.swing.Icon +private val COMMAND_CACHE = Key.create>>("SYMFONY_RUN_ANYTHING_COMMANDS") + /** * Run Anything provider for Symfony console commands. * @@ -31,17 +41,29 @@ import javax.swing.Icon class SymfonyConsoleRunAnythingProvider : RunAnythingProviderBase() { override fun getValues(dataContext: DataContext, pattern: String): Collection { - val project = CommonDataKeys.PROJECT.getData(dataContext) - if (!Symfony2ProjectComponent.isEnabled(project)) return emptyList() + val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return emptyList() + if (!Symfony2ProjectComponent.isEnabled(project) || DumbService.isDumb(project)) return emptyList() val lowerPattern = pattern.lowercase().trim() - return ApplicationManager.getApplication().runReadAction> { - SymfonyCommandUtil.getCommands(project!!) - .filter { lowerPattern in it.name.lowercase() } - } + return getCommands(project).filter { lowerPattern in it.name.lowercase() } } + private fun getCommands(project: Project): List = + CachedValuesManager.getManager(project).getCachedValue( + project, + COMMAND_CACHE, + { + ReadAction.nonBlocking>> { + CachedValueProvider.Result.create( + SymfonyCommandUtil.getCommands(project).toList(), + PsiModificationTracker.getInstance(project).forLanguage(PhpLanguage.INSTANCE) + ) + }.expireWhen { project.isDisposed }.executeSynchronously() + }, + false + ) + override fun execute(dataContext: DataContext, value: SymfonyCommand) { val project = CommonDataKeys.PROJECT.getData(dataContext) ?: return diff --git a/src/test/kotlin/fr/adrienbrault/idea/symfony2plugin/tests/integrations/terminal/SymfonyShellCommandSpecsProviderTest.kt b/src/test/kotlin/fr/adrienbrault/idea/symfony2plugin/tests/integrations/terminal/SymfonyShellCommandSpecsProviderTest.kt index 5ef459e7b..c668399af 100644 --- a/src/test/kotlin/fr/adrienbrault/idea/symfony2plugin/tests/integrations/terminal/SymfonyShellCommandSpecsProviderTest.kt +++ b/src/test/kotlin/fr/adrienbrault/idea/symfony2plugin/tests/integrations/terminal/SymfonyShellCommandSpecsProviderTest.kt @@ -1,5 +1,6 @@ package fr.adrienbrault.idea.symfony2plugin.tests.integrations.terminal +import com.intellij.testFramework.DumbModeTestUtils import fr.adrienbrault.idea.symfony2plugin.integrations.terminal.SymfonyShellCommandSpecsProvider import fr.adrienbrault.idea.symfony2plugin.integrations.terminal.collectCommandData import fr.adrienbrault.idea.symfony2plugin.tests.SymfonyLightCodeInsightFixtureTestCase @@ -85,6 +86,37 @@ class SymfonyShellCommandSpecsProviderTest : SymfonyLightCodeInsightFixtureTestC assertTrue("terminal:modern-options should be collected", "terminal:modern-options" in names) } + fun testCollectCommandDataInvalidatesOnPhpModification() { + val before = collectCommandData(project).map { it.name } + assertFalse("terminal:after-cache should not exist before adding the PHP file", "terminal:after-cache" in before) + + myFixture.addFileToProject( + "src/Command/AfterCacheCommand.php", + """ +