diff --git a/java/java-tests/testSrc/com/intellij/execution/impl/RunConfigurableTest.kt b/java/java-tests/testSrc/com/intellij/execution/impl/RunConfigurableTest.kt index fa97ba7cfc3e..ff966fb4d78c 100644 --- a/java/java-tests/testSrc/com/intellij/execution/impl/RunConfigurableTest.kt +++ b/java/java-tests/testSrc/com/intellij/execution/impl/RunConfigurableTest.kt @@ -34,7 +34,7 @@ private val ORDER = arrayOf(CONFIGURATION_TYPE, //Application ) @RunsInEdt -class RunConfigurableTest { +internal class RunConfigurableTest { companion object { @JvmField @ClassRule diff --git a/platform/lang-api/src/com/intellij/execution/configurations/RunConfigurationBase.java b/platform/lang-api/src/com/intellij/execution/configurations/RunConfigurationBase.java index ebf34746d39e..91d791b83257 100644 --- a/platform/lang-api/src/com/intellij/execution/configurations/RunConfigurationBase.java +++ b/platform/lang-api/src/com/intellij/execution/configurations/RunConfigurationBase.java @@ -215,6 +215,10 @@ public abstract class RunConfigurationBase extends UserDataHolderBase implements myOptions = XmlSerializer.deserialize(element, getOptionsClass()); } + public final void setState(@NotNull BaseState state) { + myOptions = (RunConfigurationOptions)state; + } + // we can break compatibility and make this method final (API is new and used only by our plugins), but let's avoid any inconvenience and mark as "final" after/prior to 2018.3 release. /** * Do not override this method, use {@link ConfigurationFactory#getOptionsClass()}. diff --git a/platform/lang-api/src/com/intellij/execution/configurations/UnknownRunConfiguration.java b/platform/lang-api/src/com/intellij/execution/configurations/UnknownRunConfiguration.java index 4e9929db28f1..5bd37b735393 100644 --- a/platform/lang-api/src/com/intellij/execution/configurations/UnknownRunConfiguration.java +++ b/platform/lang-api/src/com/intellij/execution/configurations/UnknownRunConfiguration.java @@ -4,7 +4,6 @@ package com.intellij.execution.configurations; import com.intellij.execution.ExecutionException; import com.intellij.execution.Executor; import com.intellij.execution.runners.ExecutionEnvironment; -import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.SettingsEditor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.InvalidDataException; @@ -21,7 +20,7 @@ import java.util.concurrent.atomic.AtomicInteger; /** * @author spleaner */ -public class UnknownRunConfiguration implements RunConfiguration, WithoutOwnBeforeRunSteps { +public final class UnknownRunConfiguration implements RunConfiguration, WithoutOwnBeforeRunSteps { private final ConfigurationFactory myFactory; private Element myStoredElement; private String myName; @@ -121,7 +120,7 @@ public class UnknownRunConfiguration implements RunConfiguration, WithoutOwnBefo } } - private static class UnknownSettingsEditor extends SettingsEditor { + private static final class UnknownSettingsEditor extends SettingsEditor { private final JPanel myPanel; private UnknownSettingsEditor() { @@ -136,7 +135,7 @@ public class UnknownRunConfiguration implements RunConfiguration, WithoutOwnBefo } @Override - protected void applyEditorTo(@NotNull final UnknownRunConfiguration s) throws ConfigurationException { + protected void applyEditorTo(@NotNull final UnknownRunConfiguration s) { } @Override diff --git a/platform/lang-impl/src/com/intellij/execution/impl/RunManagerImpl.kt b/platform/lang-impl/src/com/intellij/execution/impl/RunManagerImpl.kt index 5962c8e8286b..8e1b5ad3eeae 100644 --- a/platform/lang-impl/src/com/intellij/execution/impl/RunManagerImpl.kt +++ b/platform/lang-impl/src/com/intellij/execution/impl/RunManagerImpl.kt @@ -640,7 +640,7 @@ open class RunManagerImpl(val project: Project) : RunManagerEx(), PersistentStat private fun runConfigurationFirstLoaded() { if (selectedConfiguration == null) { - selectedConfiguration = allSettings.firstOrNull { it.type !is UnknownRunConfiguration } + selectedConfiguration = allSettings.firstOrNull { it.type !== UnknownConfigurationType.getInstance() } } } @@ -730,8 +730,15 @@ open class RunManagerImpl(val project: Project) : RunManagerEx(), PersistentStat internal fun addConfiguration(element: Element, settings: RunnerAndConfigurationSettingsImpl, isCheckRecentsLimit: Boolean = true) { if (settings.isTemplate) { val factory = settings.factory - lock.write { - templateIdToConfiguration.put(getFactoryKey(factory), settings) + // do not register unknown RC type templates (it is saved in any case in the scheme manager, so, not lost on save) + if (factory !== UnknownConfigurationType.getInstance()) { + val key = getFactoryKey(factory) + lock.write { + val old = templateIdToConfiguration.put(key, settings) + if (old != null) { + LOG.error("Template $key already registered, old: $old, new: $settings") + } + } } } else { diff --git a/platform/lang-impl/src/com/intellij/execution/impl/RunnerAndConfigurationSettingsImpl.kt b/platform/lang-impl/src/com/intellij/execution/impl/RunnerAndConfigurationSettingsImpl.kt index 56902727b217..7371b01ababb 100644 --- a/platform/lang-impl/src/com/intellij/execution/impl/RunnerAndConfigurationSettingsImpl.kt +++ b/platform/lang-impl/src/com/intellij/execution/impl/RunnerAndConfigurationSettingsImpl.kt @@ -192,15 +192,11 @@ class RunnerAndConfigurationSettingsImpl @JvmOverloads constructor(val manager: } } - val configuration = when { - isTemplate -> manager.getConfigurationTemplate(factory).configuration - else -> { - // shouldn't call createConfiguration since it calls StepBeforeRunProviders that - // may not be loaded yet. This creates initialization order issue. - val configuration = factory.createTemplateConfiguration(manager.project, manager) - configuration.name = element.getAttributeValue(NAME_ATTR) ?: return - configuration - } + val configuration = factory.createTemplateConfiguration(manager.project, manager) + if (!isTemplate) { + // shouldn't call createConfiguration since it calls StepBeforeRunProviders that + // may not be loaded yet. This creates initialization order issue. + configuration.name = element.getAttributeValue(NAME_ATTR) ?: return } _configuration = configuration diff --git a/platform/platform-impl/src/com/intellij/execution/RunManagerListener.java b/platform/platform-impl/src/com/intellij/execution/RunManagerListener.java index 49c23046f06a..0e6598b42951 100644 --- a/platform/platform-impl/src/com/intellij/execution/RunManagerListener.java +++ b/platform/platform-impl/src/com/intellij/execution/RunManagerListener.java @@ -35,6 +35,9 @@ public interface RunManagerListener extends EventListener { default void endUpdate() { } + /** + * Called also in case when RunManager doesn't have state. + */ default void stateLoaded(@NotNull RunManager runManager, boolean isFirstLoadState) { } } diff --git a/plugins/configuration-script/resources/META-INF/plugin.xml b/plugins/configuration-script/resources/META-INF/plugin.xml index 95a25e29ec53..79e63ad2df3b 100644 --- a/plugins/configuration-script/resources/META-INF/plugin.xml +++ b/plugins/configuration-script/resources/META-INF/plugin.xml @@ -12,7 +12,8 @@ - + + \ No newline at end of file diff --git a/plugins/configuration-script/src/com/intellij/configurationScript/ConfigurationFileManager.kt b/plugins/configuration-script/src/com/intellij/configurationScript/ConfigurationFileManager.kt new file mode 100644 index 000000000000..a69d218ee823 --- /dev/null +++ b/plugins/configuration-script/src/com/intellij/configurationScript/ConfigurationFileManager.kt @@ -0,0 +1,56 @@ +package com.intellij.configurationScript + +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.ClearableLazyValue +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.openapi.vfs.newvfs.BulkFileListener +import com.intellij.openapi.vfs.newvfs.events.VFileCopyEvent +import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent +import com.intellij.openapi.vfs.newvfs.events.VFileEvent +import com.intellij.util.containers.ContainerUtil + +internal const val IDE_FILE = "intellij.yaml" +internal const val IDE_FILE_VARIANT_2 = "intellij.yml" + +internal class ConfigurationFileManager(project: Project) { + private val clearableLazyValues = ContainerUtil.createConcurrentList>() + + fun registerClearableLazyValue(value: ClearableLazyValue<*>) { + clearableLazyValues.add(value) + } + + init { + project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener { + override fun after(events: List) { + for (event in events) { + if (event is VFileCopyEvent) { + continue + } + + if (event is VFileCreateEvent) { + // VFileCreateEvent computes file on request, so, avoid getFile call + if (event.isDirectory || !(event.childName == IDE_FILE || event.childName == IDE_FILE_VARIANT_2)) { + continue + } + } + else { + val file = event.file ?: continue + if (!isConfigurationFile(file)) { + continue + } + } + + clearableLazyValues.forEach { it.drop() } + } + } + }) + } +} + +// todo check parent? +internal fun isConfigurationFile(file: VirtualFile): Boolean { + val nameSequence = file.nameSequence + return StringUtil.equals(nameSequence, IDE_FILE) || StringUtil.equals(nameSequence, IDE_FILE_VARIANT_2) +} \ No newline at end of file diff --git a/plugins/configuration-script/src/com/intellij/configurationScript/IntellijConfigurationAppInitializer.kt b/plugins/configuration-script/src/com/intellij/configurationScript/IntellijConfigurationAppInitializer.kt index 5c091514ca62..ca91535510c9 100644 --- a/plugins/configuration-script/src/com/intellij/configurationScript/IntellijConfigurationAppInitializer.kt +++ b/plugins/configuration-script/src/com/intellij/configurationScript/IntellijConfigurationAppInitializer.kt @@ -1,15 +1,14 @@ package com.intellij.configurationScript -import com.intellij.execution.RunManager -import com.intellij.execution.RunManagerListener import com.intellij.execution.configurations.ConfigurationFactory -import com.intellij.execution.impl.RUN_CONFIGURATION_TEMPLATE_PROVIDER_EP +import com.intellij.execution.configurations.RunConfigurationBase import com.intellij.execution.impl.RunConfigurationTemplateProvider import com.intellij.execution.impl.RunManagerImpl import com.intellij.execution.impl.RunnerAndConfigurationSettingsImpl -import com.intellij.ide.ApplicationInitializedListener -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.BaseState +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project +import com.intellij.openapi.util.AtomicClearableLazyValue import com.intellij.openapi.util.registry.Registry import com.intellij.util.io.exists import com.intellij.util.io.inputStreamIfExists @@ -22,61 +21,55 @@ import java.io.Reader import java.nio.file.Path import java.nio.file.Paths -internal class IntellijConfigurationAppInitializer : ApplicationInitializedListener { - override fun componentsInitialized() { - ApplicationManager.getApplication().messageBus.connect().subscribe(RunManagerListener.TOPIC, object: RunManagerListener { - override fun stateLoaded(runManager: RunManager, isFirstLoadState: Boolean) { - if (!isFirstLoadState || !Registry.`is`("run.manager.use.intellij.config.file", false)) { - return - } - - val project = (runManager as? RunManagerImpl)?.project ?: return - // todo listen file changes - val file = findConfigurationFile(project) ?: return - val inputStream = file.inputStreamIfExists() ?: return - val map = THashMap() - inputStream.use { - parseConfigurationFile(it.bufferedReader()) { factory, state -> - map.put(factory, FactoryEntry(state)) - } - } - RUN_CONFIGURATION_TEMPLATE_PROVIDER_EP.findExtension(MyRunConfigurationTemplateProvider::class.java, project)!!.setTemplates(map) - } - }) - } -} - private class FactoryEntry(state: Any) { var state: Any? = state var settings: RunnerAndConfigurationSettingsImpl? = null } -private class MyRunConfigurationTemplateProvider : RunConfigurationTemplateProvider { - @Volatile - private var map: Map? = null +private class MyRunConfigurationTemplateProvider(private val project: Project) : RunConfigurationTemplateProvider { + private val map = object : AtomicClearableLazyValue>() { + override fun compute(): Map { + val file = findConfigurationFile(project) ?: return emptyMap() + val inputStream = file.inputStreamIfExists() ?: return emptyMap() + val map = THashMap() + inputStream.use { + parseConfigurationFile(it.bufferedReader(), isTemplatesOnly = true) { factory, state -> + map.put(factory, FactoryEntry(state)) + } + } + return map + } + } + + init { + project.service().registerClearableLazyValue(map) + } override fun getRunConfigurationTemplate(factory: ConfigurationFactory, runManager: RunManagerImpl): RunnerAndConfigurationSettingsImpl? { - val item = map?.get(factory) ?: return null + if (!Registry.`is`("run.manager.use.intellij.config.file", false)) { + return null + } + + val item = map.value.get(factory) ?: return null synchronized(item) { var settings = item.settings if (settings != null) { return settings } - settings = RunnerAndConfigurationSettingsImpl(runManager, isTemplate = true) + val configuration = factory.createTemplateConfiguration(runManager.project, runManager) + (configuration as RunConfigurationBase).setState(item.state as BaseState) + settings = RunnerAndConfigurationSettingsImpl(runManager, configuration, isTemplate = true, isSingleton = factory.singletonPolicy.isSingleton) + item.state = null item.settings = settings return settings } } - - internal fun setTemplates(value: Map) { - map = value - } } // we cannot use the same approach as we generate JSON scheme because we should load option classes only in a lazy manner // that's why we don't use snakeyaml TypeDescription approach to load -internal fun parseConfigurationFile(reader: Reader, processor: (factory: ConfigurationFactory, state: Any) -> Unit) { +internal fun parseConfigurationFile(reader: Reader, isTemplatesOnly: Boolean, processor: (factory: ConfigurationFactory, state: Any) -> Unit) { val yaml = Yaml(SafeConstructor()) // later we can avoid full node graph building, but for now just use simple implementation (problem is that Yaml supports references and merge - proper support of it can be tricky) // "load" under the hood uses "compose" - i.e. Yaml itself doesn't use stream API to build object model. @@ -86,7 +79,7 @@ internal fun parseConfigurationFile(reader: Reader, processor: (factory: Configu val keyNode = tuple.keyNode if (keyNode is ScalarNode && keyNode.value == Keys.runConfigurations) { val rcTypeGroupNode = tuple.valueNode as? MappingNode ?: continue - dataReader.read(rcTypeGroupNode) + dataReader.read(rcTypeGroupNode, isTemplatesOnly) } } } @@ -102,7 +95,4 @@ private fun findConfigurationFile(project: Project): Path? { file = projectIdeaDir.resolve("intellij.yml") } return file -} - -internal const val IDE_FILE = "intellij.yaml" -internal const val IDE_FILE_VARIANT_2 = "intellij.yml" \ No newline at end of file +} \ No newline at end of file diff --git a/plugins/configuration-script/src/com/intellij/configurationScript/IntellijConfigurationJsonSchemaProviderFactory.kt b/plugins/configuration-script/src/com/intellij/configurationScript/IntellijConfigurationJsonSchemaProviderFactory.kt index 5fd7c4fc0773..8e3764ee8a5e 100644 --- a/plugins/configuration-script/src/com/intellij/configurationScript/IntellijConfigurationJsonSchemaProviderFactory.kt +++ b/plugins/configuration-script/src/com/intellij/configurationScript/IntellijConfigurationJsonSchemaProviderFactory.kt @@ -59,10 +59,7 @@ internal class IntellijConfigurationJsonSchemaProviderFactory : JsonSchemaProvid override fun isUserVisible() = false - override fun isAvailable(file: VirtualFile): Boolean { - val nameSequence = file.nameSequence - return StringUtil.equals(nameSequence, IDE_FILE) || StringUtil.equals(nameSequence, IDE_FILE_VARIANT_2) - } + override fun isAvailable(file: VirtualFile) = isConfigurationFile(file) }) return (project as UserDataHolderBase).putUserDataIfAbsent(PROVIDER_KEY, result) } diff --git a/plugins/configuration-script/src/com/intellij/configurationScript/RunConfigurationListReader.kt b/plugins/configuration-script/src/com/intellij/configurationScript/RunConfigurationListReader.kt index e92f21ebb29e..a2eb95a1ffad 100644 --- a/plugins/configuration-script/src/com/intellij/configurationScript/RunConfigurationListReader.kt +++ b/plugins/configuration-script/src/com/intellij/configurationScript/RunConfigurationListReader.kt @@ -15,7 +15,7 @@ import org.yaml.snakeyaml.nodes.SequenceNode internal class RunConfigurationListReader(private val processor: (factory: ConfigurationFactory, state: Any) -> Unit) { // rc grouped by type - fun read(parentNode: MappingNode) { + fun read(parentNode: MappingNode, isTemplatesOnly: Boolean) { val keyToType = THashMap() processConfigurationTypes { configurationType, propertyName, _ -> keyToType.put(propertyName.toString(), configurationType) @@ -28,6 +28,13 @@ internal class RunConfigurationListReader(private val processor: (factory: Confi continue } + if (keyNode.value == Keys.templates) { + if (isTemplatesOnly) { + read(tuple.valueNode as? MappingNode ?: continue, false) + } + continue + } + val configurationType = keyToType.get(keyNode.value) if (configurationType == null) { LOG.warn("Unknown run configuration type: ${keyNode.value}") diff --git a/plugins/configuration-script/test/ConfigurationFileTest.kt b/plugins/configuration-script/test/ConfigurationFileTest.kt index 99d476c2be80..3c785bf7e45b 100644 --- a/plugins/configuration-script/test/ConfigurationFileTest.kt +++ b/plugins/configuration-script/test/ConfigurationFileTest.kt @@ -95,11 +95,33 @@ class ConfigurationFileTest { options.isAlternativeJrePathEnabled = true assertThat(result).containsExactly(options) } + + @Test + fun `one jvmMainMethod as list - template`() { + val result = parse(""" + runConfigurations: + templates: + jvmMainMethod: + - isAlternativeJrePathEnabled: true + """, isTemplatesOnly = true) + val options = ApplicationConfigurationOptions() + options.isAlternativeJrePathEnabled = true + assertThat(result).containsExactly(options) + } + + @Test + fun `templates as invalid node type`() { + val result = parse(""" + runConfigurations: + templates: foo + """, isTemplatesOnly = true) + assertThat(result).isEmpty() + } } -private fun parse(@Language("YAML") data: String): List { +private fun parse(@Language("YAML") data: String, isTemplatesOnly: Boolean = false): List { val list = SmartList() - parseConfigurationFile(data.trimIndent().reader()) { _, state -> + parseConfigurationFile(data.trimIndent().reader(), isTemplatesOnly) { _, state -> list.add(state) } return list