blob: 44d9b31db464f85cef8e1635ea4833068147cb5e [file]
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
}