configuration file: track file changes, continue RunConfigurationTemplateProvider

This commit is contained in:
Vladimir Krivosheev
2018-09-06 13:35:00 +02:00
parent 2c6b0c44b6
commit 07b79c1a8f
12 changed files with 151 additions and 69 deletions
@@ -34,7 +34,7 @@ private val ORDER = arrayOf(CONFIGURATION_TYPE, //Application
)
@RunsInEdt
class RunConfigurableTest {
internal class RunConfigurableTest {
companion object {
@JvmField
@ClassRule
@@ -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()}.
@@ -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<UnknownRunConfiguration> {
private static final class UnknownSettingsEditor extends SettingsEditor<UnknownRunConfiguration> {
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
@@ -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 {
@@ -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
@@ -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) {
}
}
@@ -12,7 +12,8 @@
<ProviderFactory implementation="com.intellij.configurationScript.IntellijConfigurationJsonSchemaProviderFactory"/>
</extensions>
<extensions defaultExtensionNs="com.intellij">
<applicationInitializedListener implementation="com.intellij.configurationScript.IntellijConfigurationAppInitializer"/>
<projectService serviceImplementation="com.intellij.configurationScript.ConfigurationFileManager"/>
<runConfigurationTemplateProvider implementation="com.intellij.configurationScript.MyRunConfigurationTemplateProvider"/>
</extensions>
</idea-plugin>
@@ -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<ClearableLazyValue<*>>()
fun registerClearableLazyValue(value: ClearableLazyValue<*>) {
clearableLazyValues.add(value)
}
init {
project.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener {
override fun after(events: List<VFileEvent>) {
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)
}
@@ -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<ConfigurationFactory, FactoryEntry>()
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<ConfigurationFactory, FactoryEntry>? = null
private class MyRunConfigurationTemplateProvider(private val project: Project) : RunConfigurationTemplateProvider {
private val map = object : AtomicClearableLazyValue<Map<ConfigurationFactory, FactoryEntry>>() {
override fun compute(): Map<ConfigurationFactory, FactoryEntry> {
val file = findConfigurationFile(project) ?: return emptyMap()
val inputStream = file.inputStreamIfExists() ?: return emptyMap()
val map = THashMap<ConfigurationFactory, FactoryEntry>()
inputStream.use {
parseConfigurationFile(it.bufferedReader(), isTemplatesOnly = true) { factory, state ->
map.put(factory, FactoryEntry(state))
}
}
return map
}
}
init {
project.service<ConfigurationFileManager>().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<ConfigurationFactory, FactoryEntry>) {
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"
}
@@ -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)
}
@@ -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<String, ConfigurationType>()
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}")
@@ -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<Any> {
private fun parse(@Language("YAML") data: String, isTemplatesOnly: Boolean = false): List<Any> {
val list = SmartList<Any>()
parseConfigurationFile(data.trimIndent().reader()) { _, state ->
parseConfigurationFile(data.trimIndent().reader(), isTemplatesOnly) { _, state ->
list.add(state)
}
return list