| package com.intellij.configurationScript | |
| import com.intellij.configurationScript.yaml.LightScalarResolver | |
| import com.intellij.openapi.components.service | |
| import com.intellij.openapi.project.Project | |
| 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.concurrency.SynchronizedClearableLazy | |
| import com.intellij.util.containers.ContainerUtil | |
| import com.intellij.util.io.inputStreamIfExists | |
| import org.snakeyaml.engine.v2.api.LoadSettings | |
| import org.snakeyaml.engine.v2.composer.Composer | |
| import org.snakeyaml.engine.v2.nodes.MappingNode | |
| import org.snakeyaml.engine.v2.nodes.NodeTuple | |
| import org.snakeyaml.engine.v2.parser.ParserImpl | |
| import org.snakeyaml.engine.v2.scanner.StreamReader | |
| import java.io.Reader | |
| import java.nio.file.Path | |
| import java.nio.file.Paths | |
| // 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 class ConfigurationFileManager(project: Project) { | |
| private val clearableLazyValues = ContainerUtil.createConcurrentList<() -> Unit>() | |
| private val yamlData = SynchronizedClearableLazy { | |
| val file = findConfigurationFile(project) ?: return@SynchronizedClearableLazy null | |
| try { | |
| val inputStream = file.inputStreamIfExists() ?: return@SynchronizedClearableLazy null | |
| return@SynchronizedClearableLazy doRead(inputStream.bufferedReader()) | |
| } | |
| catch (e: Throwable) { | |
| LOG.error("Cannot parse \"$file\"", e) | |
| } | |
| null | |
| } | |
| init { | |
| registerClearableLazyValue(yamlData) | |
| } | |
| companion object { | |
| fun getInstance(project: Project) = project.service<ConfigurationFileManager>() | |
| } | |
| fun registerClearableLazyValue(value: SynchronizedClearableLazy<*>) { | |
| registerClearableLazyValue { value.drop() } | |
| } | |
| fun registerClearableLazyValue(value: () -> Unit) { | |
| clearableLazyValues.add(value) | |
| } | |
| init { | |
| addFileListener(project) | |
| } | |
| private fun addFileListener(project: Project) { | |
| 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 || !isConfigurationFile(event.childName)) { | |
| continue | |
| } | |
| } | |
| else { | |
| val file = event.file ?: continue | |
| if (!isConfigurationFile(file)) { | |
| continue | |
| } | |
| } | |
| clearableLazyValues.forEach { it() } | |
| } | |
| } | |
| }) | |
| } | |
| fun getConfigurationNode() = yamlData.value | |
| // 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. | |
| fun findValueNode(namePath: String): List<NodeTuple>? { | |
| return findValueNodeByPath(namePath, yamlData.value?.value ?: return null) | |
| } | |
| } | |
| internal fun doRead(reader: Reader): MappingNode? { | |
| reader.use { | |
| val settings = LoadSettings.builder() | |
| .setUseMarks(false) | |
| .setScalarResolver(LightScalarResolver()) | |
| .build() | |
| val parser = ParserImpl(StreamReader(it, settings), settings) | |
| return Composer(parser, settings).singleNode.orElse(null) as? MappingNode | |
| } | |
| } | |
| // todo check parent? | |
| internal fun isConfigurationFile(file: VirtualFile) = isConfigurationFile(file.nameSequence) | |
| private const val filePrefix = "intellij." | |
| private val fileExtensions = listOf("yaml", "yml", "json") | |
| internal fun isConfigurationFile(name: CharSequence): Boolean { | |
| return name.startsWith(filePrefix) && fileExtensions.any { name.length == (filePrefix.length + it.length) && name.endsWith(it) } | |
| } | |
| /** | |
| * not-null doesn't mean that you should not expect NoSuchFileException | |
| */ | |
| private fun findConfigurationFile(project: Project): Path? { | |
| val projectIdeaDir = Paths.get(project.basePath ?: return null) | |
| var file = projectIdeaDir.resolve("intellij.yaml") | |
| if (!file.toFile().exists()) { | |
| // do not check file exists - on read we in any case should check NoSuchFileException | |
| file = projectIdeaDir.resolve("intellij.yml") | |
| } | |
| return file | |
| } | |