diff --git a/src/main/kotlin/MinecraftTreeStructureProvider.kt b/src/main/kotlin/MinecraftTreeStructureProvider.kt
new file mode 100644
index 000000000..7f4cec770
--- /dev/null
+++ b/src/main/kotlin/MinecraftTreeStructureProvider.kt
@@ -0,0 +1,47 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2025 minecraft-dev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, version 3.0 only.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.demonwav.mcdev
+
+import com.demonwav.mcdev.region.RegionFileType
+import com.demonwav.mcdev.region.RegionPsiFileNode
+import com.intellij.ide.projectView.TreeStructureProvider
+import com.intellij.ide.projectView.ViewSettings
+import com.intellij.ide.projectView.impl.nodes.PsiFileNode
+import com.intellij.ide.util.treeView.AbstractTreeNode
+
+private fun mapMcaNode(node: AbstractTreeNode<*>): AbstractTreeNode<*> {
+ if (node is PsiFileNode) {
+ val value = node.value
+ if (value?.fileType is RegionFileType) {
+ return RegionPsiFileNode(node.project, value, node.settings)
+ }
+ }
+
+ return node
+}
+
+class MinecraftTreeStructureProvider : TreeStructureProvider {
+ override fun modify(
+ parent: AbstractTreeNode<*>,
+ children: MutableCollection>,
+ settings: ViewSettings?
+ ) = children.mapTo(ArrayList(children.size), ::mapMcaNode)
+}
diff --git a/src/main/kotlin/nbt/NbtVirtualFile.kt b/src/main/kotlin/nbt/NbtVirtualFile.kt
index e95837dc7..d3ec6a907 100644
--- a/src/main/kotlin/nbt/NbtVirtualFile.kt
+++ b/src/main/kotlin/nbt/NbtVirtualFile.kt
@@ -25,6 +25,7 @@ import com.demonwav.mcdev.nbt.editor.CompressionSelection
import com.demonwav.mcdev.nbt.editor.NbtToolbar
import com.demonwav.mcdev.nbt.lang.NbttFile
import com.demonwav.mcdev.nbt.lang.NbttLanguage
+import com.demonwav.mcdev.region.RegionFileSystem
import com.demonwav.mcdev.util.loggerForTopLevel
import com.demonwav.mcdev.util.runReadActionAsync
import com.demonwav.mcdev.util.runWriteTaskLater
@@ -89,6 +90,10 @@ class NbtVirtualFile(
override fun isTooLargeForIntelligence() = ThreeState.NO
fun writeFile(requester: Any) {
+ if (!isWritable) {
+ throw IllegalStateException("Backing file is not writable")
+ }
+
runReadActionAsync {
val nbttFile = PsiManager.getInstance(project).findFile(this) as? NbttFile
@@ -132,6 +137,7 @@ class NbtVirtualFile(
val filteredStream = when (toolbar.selection) {
CompressionSelection.GZIP -> GZIPOutputStream(this.parent.getOutputStream(requester))
CompressionSelection.UNCOMPRESSED -> this.parent.getOutputStream(requester)
+ else -> throw NotImplementedError("Region-only compression algorithms are not supported for standalone NBT files")
}
DataOutputStream(filteredStream).use { stream ->
@@ -147,4 +153,22 @@ class NbtVirtualFile(
}
}
}
+
+ // If the NBT file is part of a region file, this will be non-null and represent the file's compression algorithm
+ val compressionInRegionFile: CompressionSelection? by lazy {
+ val compressionAlgorithm = (backingFile.fileSystem as? RegionFileSystem)
+ ?.getHandler(backingFile)
+ ?.resolveChunk(backingFile.name)
+ ?.payloadCompressionAlgorithm
+
+ when (compressionAlgorithm) {
+ null -> null
+ 1 -> CompressionSelection.GZIP
+ 2 -> CompressionSelection.ZLIB
+ 3 -> CompressionSelection.UNCOMPRESSED
+ 4 -> CompressionSelection.LZ4
+ // We shouldn't be able to open NBT files if the compression algorithm is unsupported anyway
+ else -> CompressionSelection.UNCOMPRESSED
+ }
+ }
}
diff --git a/src/main/kotlin/nbt/editor/CompressionComboBoxModel.kt b/src/main/kotlin/nbt/editor/CompressionComboBoxModel.kt
new file mode 100644
index 000000000..07edf1a3a
--- /dev/null
+++ b/src/main/kotlin/nbt/editor/CompressionComboBoxModel.kt
@@ -0,0 +1,12 @@
+package com.demonwav.mcdev.nbt.editor
+
+import com.intellij.ui.CollectionComboBoxModel
+
+private fun makeItems(isInRegionFile: Boolean) = if (isInRegionFile) {
+ CompressionSelection.entries.toList()
+} else {
+ CompressionSelection.entries.asSequence().filter { !it.regionFileOnly }.toList()
+}
+
+class CompressionComboBoxModel(isInRegionFile: Boolean) :
+ CollectionComboBoxModel(makeItems(isInRegionFile))
diff --git a/src/main/kotlin/nbt/editor/CompressionSelection.kt b/src/main/kotlin/nbt/editor/CompressionSelection.kt
index 5945f0db4..ed0cd02b5 100644
--- a/src/main/kotlin/nbt/editor/CompressionSelection.kt
+++ b/src/main/kotlin/nbt/editor/CompressionSelection.kt
@@ -22,9 +22,11 @@ package com.demonwav.mcdev.nbt.editor
import com.demonwav.mcdev.asset.MCDevBundle
-enum class CompressionSelection(private val selectionNameFunc: () -> String) {
+enum class CompressionSelection(private val selectionNameFunc: () -> String, val regionFileOnly: Boolean = false) {
GZIP({ MCDevBundle("nbt.compression.gzip") }),
UNCOMPRESSED({ MCDevBundle("nbt.compression.uncompressed") }),
+ ZLIB({ MCDevBundle("nbt.compression.zlib") }, regionFileOnly = true),
+ LZ4({ MCDevBundle("nbt.compression.lz4") }, regionFileOnly = true),
;
override fun toString(): String = selectionNameFunc()
diff --git a/src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt b/src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt
index a663a1dd6..3d5c4b826 100644
--- a/src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt
+++ b/src/main/kotlin/nbt/editor/NbtFileEditorProvider.kt
@@ -97,7 +97,7 @@ private class NbtFileEditor(
AnActionListener.TOPIC,
object : AnActionListener {
override fun afterActionPerformed(action: AnAction, event: AnActionEvent, result: AnActionResult) {
- if (action !is SaveAllAction) {
+ if (action !is SaveAllAction || !file.isWritable) {
return
}
diff --git a/src/main/kotlin/nbt/editor/NbtToolbar.kt b/src/main/kotlin/nbt/editor/NbtToolbar.kt
index 0c4a9568e..7ddb39051 100644
--- a/src/main/kotlin/nbt/editor/NbtToolbar.kt
+++ b/src/main/kotlin/nbt/editor/NbtToolbar.kt
@@ -24,14 +24,14 @@ import com.demonwav.mcdev.asset.MCDevBundle
import com.demonwav.mcdev.nbt.NbtVirtualFile
import com.demonwav.mcdev.util.runWriteTaskLater
import com.intellij.openapi.ui.DialogPanel
-import com.intellij.ui.EnumComboBoxModel
import com.intellij.ui.dsl.builder.bindItem
import com.intellij.ui.dsl.builder.panel
class NbtToolbar(nbtFile: NbtVirtualFile) {
private var compressionSelection: CompressionSelection? =
- if (nbtFile.isCompressed) CompressionSelection.GZIP else CompressionSelection.UNCOMPRESSED
+ nbtFile.compressionInRegionFile
+ ?: if (nbtFile.isCompressed) CompressionSelection.GZIP else CompressionSelection.UNCOMPRESSED
val selection: CompressionSelection
get() = compressionSelection!!
@@ -41,13 +41,18 @@ class NbtToolbar(nbtFile: NbtVirtualFile) {
init {
panel = panel {
row(MCDevBundle("nbt.compression.file_type.label")) {
- comboBox(EnumComboBoxModel(CompressionSelection::class.java))
+ val isInRegionFile = nbtFile.compressionInRegionFile != null
+
+ comboBox(CompressionComboBoxModel(isInRegionFile))
.bindItem(::compressionSelection)
.enabled(nbtFile.isWritable && nbtFile.parseSuccessful)
- button(MCDevBundle("nbt.compression.save.button")) {
- panel.apply()
- runWriteTaskLater {
- nbtFile.writeFile(this)
+
+ if (nbtFile.isWritable) {
+ button(MCDevBundle("nbt.compression.save.button")) {
+ panel.apply()
+ runWriteTaskLater {
+ nbtFile.writeFile(this)
+ }
}
}
}
diff --git a/src/main/kotlin/region/RegionArchiveHandler.kt b/src/main/kotlin/region/RegionArchiveHandler.kt
new file mode 100644
index 000000000..9924308ef
--- /dev/null
+++ b/src/main/kotlin/region/RegionArchiveHandler.kt
@@ -0,0 +1,89 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2025 minecraft-dev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, version 3.0 only.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.demonwav.mcdev.region
+
+import com.intellij.openapi.vfs.impl.ArchiveHandler
+import java.io.FileNotFoundException
+import java.io.InputStream
+
+private val COORDINATE_FILE_NAME_REGEX = Regex("""^[a-z]\.(?-?\d+)\.(?-?\d+)\.\w+${'$'}""")
+
+/**
+ * Parses the coordinates of a region/chunk file (e.g. `r.1.-1.mca`), if representable by Int
+ */
+private fun parseFileNameCoordinates(fileName: CharSequence): Pair? = COORDINATE_FILE_NAME_REGEX
+ .matchAt(fileName, 0)
+ ?.let { matchResult ->
+ val (x, z) = matchResult.destructured
+ return try {
+ Pair(x.toInt(), z.toInt())
+ } catch (e: NumberFormatException) {
+ // This may happen if the file name contains a number that's too big for an Int
+ null
+ }
+ }
+
+class RegionArchiveHandler(path: String) : ArchiveHandler(path) {
+ private val regionFile = RegionFile(file)
+
+ // Absolute region coordinates. May be null if the file has a nonstandard name.
+ private val regionXZ = parseFileNameCoordinates(path.substringAfterLast('/'))
+
+ override fun createEntriesMap() = mutableMapOf().apply {
+ val root = createRootEntry()
+ this[""] = root
+
+ for (chunk in regionFile) {
+ val name = if (regionXZ != null) {
+ val x = chunk.x + regionXZ.first * 32
+ val z = chunk.z + regionXZ.second * 32
+ "c.$x.$z.nbt"
+ } else {
+ val x = chunk.x
+ val z = chunk.z
+ "c.~$x.~$z.nbt"
+ }
+
+ this[name] = EntryInfo(name, false, chunk.payloadLength.toLong(), chunk.timestamp, root)
+ }
+ }
+
+ fun resolveChunk(relativePath: String): RegionFile.Chunk? {
+ var (x, z) = parseFileNameCoordinates(relativePath.substringAfterLast('/'))
+ ?: throw FileNotFoundException("Illegal name for region file entry: $relativePath")
+
+ x = x.mod(32)
+ z = z.mod(32)
+ return regionFile[x, z]
+ }
+
+ override fun getInputStream(relativePath: String): InputStream {
+ val stream = resolveChunk(relativePath)?.read()
+
+ if (stream == null) {
+ throw FileNotFoundException("Chunk entry is not initialized")
+ } else {
+ return stream
+ }
+ }
+
+ override fun contentsToByteArray(relativePath: String) = getInputStream(relativePath).readBytes()
+}
diff --git a/src/main/kotlin/region/RegionFile.kt b/src/main/kotlin/region/RegionFile.kt
new file mode 100644
index 000000000..af41523fa
--- /dev/null
+++ b/src/main/kotlin/region/RegionFile.kt
@@ -0,0 +1,154 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2025 minecraft-dev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, version 3.0 only.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.demonwav.mcdev.region
+
+import com.intellij.util.io.LimitedInputStream
+import java.io.*
+import java.nio.ByteBuffer
+import java.util.zip.GZIPInputStream
+import java.util.zip.Inflater
+import java.util.zip.InflaterInputStream
+import net.jpountz.lz4.LZ4BlockInputStream
+
+private const val SECTOR_SIZE = 4096
+private const val CHUNKS_PER_REGION = 32 * 32
+
+private fun xzToIndex(x: Int, z: Int) = x + z * 32
+private fun indexToXz(index: Int) = (index.mod(32) to index.div(32))
+
+/**
+ * Helper class to read anvil region files (https://minecraft.wiki/w/Region_file_format)
+ */
+class RegionFile(private val filePath: File) : AutoCloseable {
+ private val file = RandomAccessFile(filePath, "r")
+ private val totalSectorCount = file.length() / SECTOR_SIZE
+ private val chunkIndex = arrayOfNulls(CHUNKS_PER_REGION).apply {
+ if (totalSectorCount < 2) {
+ // Invalid format
+ return@apply
+ }
+
+ val firstTwoSectors = ByteBuffer
+ .wrap(ByteArray(2 * SECTOR_SIZE).apply {
+ file.readFully(this)
+ })
+ .asIntBuffer()
+
+ for (i in 0 until CHUNKS_PER_REGION) {
+ val offsetAndSize = firstTwoSectors.get(i).toUInt()
+ val entry = ChunkIndexEntry.decode(offsetAndSize, firstTwoSectors.get(CHUNKS_PER_REGION + i))
+ if (entry.sectorOffset == 0 || entry.sectorCount == 0) {
+ continue
+ }
+
+ this[i] = entry
+ }
+ }
+
+ operator fun get(relativeChunkX: Int, relativeChunkZ: Int): Chunk? {
+ if (!(relativeChunkX in 0..32 && relativeChunkZ in 0..32)) {
+ throw IndexOutOfBoundsException("Chunk coordinates ($relativeChunkX, $relativeChunkZ) out of bounds for region file")
+ }
+
+ return chunkIndex[xzToIndex(relativeChunkX, relativeChunkZ)]?.let { entry ->
+ Chunk(
+ relativeChunkX,
+ relativeChunkZ,
+ entry
+ )
+ }
+ }
+
+ operator fun iterator(): Iterator = chunkIndex
+ .asSequence()
+ .mapIndexed { idx, e -> indexToXz(idx) to e }
+ .filter { it.second != null }
+ .map { (xz, e) -> Chunk(xz.first, xz.second, e!!) }
+ .iterator()
+
+ override fun close() {
+ file.close()
+ }
+
+ internal data class ChunkIndexEntry(val sectorOffset: Int, val sectorCount: Int, val timestamp: Int) {
+ companion object {
+ fun decode(offsetAndSize: UInt, timestamp: Int): ChunkIndexEntry {
+ val sectorOffset = offsetAndSize.shr(8).toInt()
+ val sectorSize = offsetAndSize.and(0b11111111u).toInt()
+ return ChunkIndexEntry(sectorOffset, sectorSize, timestamp)
+ }
+ }
+ }
+
+ inner class Chunk(
+ val x: Int,
+ val z: Int,
+ val timestamp: Long,
+ private val sectorOffset: Int,
+ private val sectorCount: Int,
+ ) {
+ private val firstByte get() = (sectorOffset.toLong() * SECTOR_SIZE.toLong())
+ val payloadLength by lazy {
+ file.seek(firstByte)
+ file.readInt()
+ }
+ val payloadCompressionAlgorithm by lazy {
+ file.seek(firstByte + 4)
+ file.readByte().toInt()
+ }
+
+ internal constructor(x: Int, z: Int, chunkIndexEntry: ChunkIndexEntry) : this(
+ x,
+ z,
+ chunkIndexEntry.timestamp.toLong(),
+ chunkIndexEntry.sectorOffset,
+ chunkIndexEntry.sectorCount,
+ )
+
+ fun read(): InputStream? {
+ val payloadLength = payloadLength
+ if (payloadLength > sectorCount * SECTOR_SIZE || (sectorOffset + sectorCount) > totalSectorCount) {
+ return null
+ }
+
+ if (payloadLength == 0) {
+ return InputStream.nullInputStream()
+ }
+
+ val chunkReader = BufferedInputStream(FileInputStream(filePath))
+ chunkReader.skip(firstByte + 5)
+ val compressedPayload = LimitedInputStream(chunkReader, payloadLength - 1)
+
+ return when (payloadCompressionAlgorithm) {
+ // GZip
+ 1 -> GZIPInputStream(compressedPayload)
+ // ZLib
+ 2 -> InflaterInputStream(compressedPayload, Inflater())
+ // Uncompressed
+ 3 -> compressedPayload
+ // LZ4
+ 4 -> LZ4BlockInputStream(compressedPayload)
+ // Custom and/or yet-to-exist algorithm
+ else -> null
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/region/RegionFileSystem.kt b/src/main/kotlin/region/RegionFileSystem.kt
new file mode 100644
index 000000000..fc76e8e45
--- /dev/null
+++ b/src/main/kotlin/region/RegionFileSystem.kt
@@ -0,0 +1,51 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2025 minecraft-dev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, version 3.0 only.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.demonwav.mcdev.region
+
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem
+import com.intellij.openapi.vfs.newvfs.VfsImplUtil
+import com.intellij.util.io.URLUtil
+
+private const val PROTOCOL = "mcdev-region"
+
+class RegionFileSystem : ArchiveFileSystem() {
+ @Suppress("CompanionObjectInExtension") // False-positive: this is a getter, not a field
+ companion object {
+ @JvmStatic
+ val INSTANCE get() = VirtualFileManager.getInstance().getFileSystem(PROTOCOL) as RegionFileSystem
+ }
+
+ override fun getProtocol() = PROTOCOL
+ override fun isReadOnly() = true
+ override fun isCorrectFileType(local: VirtualFile) = local.fileType is RegionFileType
+
+ override fun extractRootPath(path: String) = extractLocalPath(path) + URLUtil.JAR_SEPARATOR
+ override fun extractLocalPath(archivePath: String) = archivePath.substringBeforeLast(URLUtil.JAR_SEPARATOR)
+ override fun composeRootPath(localPath: String) = "$localPath${URLUtil.JAR_SEPARATOR}"
+
+ override fun findFileByPath(path: String): VirtualFile? = VfsImplUtil.findFileByPath(this, path)
+ override fun refresh(asynchronous: Boolean) = VfsImplUtil.refresh(this, asynchronous)
+ override fun refreshAndFindFileByPath(path: String) = VfsImplUtil.refreshAndFindFileByPath(this, path)
+ override fun findFileByPathIfCached(path: String) = VfsImplUtil.findFileByPathIfCached(this, path)
+ public override fun getHandler(entryFile: VirtualFile) = VfsImplUtil.getHandler(this, entryFile, ::RegionArchiveHandler)
+}
diff --git a/src/main/kotlin/region/RegionFileType.kt b/src/main/kotlin/region/RegionFileType.kt
new file mode 100644
index 000000000..c2d3b8ddb
--- /dev/null
+++ b/src/main/kotlin/region/RegionFileType.kt
@@ -0,0 +1,36 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2025 minecraft-dev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, version 3.0 only.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.demonwav.mcdev.region
+
+import com.demonwav.mcdev.asset.MCDevBundle
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.fileTypes.FileType
+import com.intellij.openapi.vfs.VirtualFile
+
+object RegionFileType : FileType {
+ override fun getDefaultExtension() = "mca"
+ override fun getIcon() = AllIcons.FileTypes.Archive
+ override fun getCharset(file: VirtualFile, content: ByteArray) = null
+ override fun getName() = "MCA"
+ override fun getDescription() = MCDevBundle("region.file_type.description")
+ override fun isBinary() = true
+ override fun isReadOnly() = true
+}
diff --git a/src/main/kotlin/region/RegionPsiFileNode.kt b/src/main/kotlin/region/RegionPsiFileNode.kt
new file mode 100644
index 000000000..67db5f5cc
--- /dev/null
+++ b/src/main/kotlin/region/RegionPsiFileNode.kt
@@ -0,0 +1,52 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2025 minecraft-dev
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation, version 3.0 only.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see .
+ */
+
+package com.demonwav.mcdev.region
+
+import com.intellij.ide.projectView.ViewSettings
+import com.intellij.ide.projectView.impl.nodes.PsiFileNode
+import com.intellij.ide.util.treeView.AbstractTreeNode
+import com.intellij.openapi.project.Project
+import com.intellij.psi.*
+import com.intellij.util.containers.ContainerUtil
+
+class RegionPsiFileNode(
+ project: Project?,
+ value: PsiFile,
+ viewSettings: ViewSettings?,
+) : PsiFileNode(project, value, viewSettings) {
+ override fun getChildrenImpl(): MutableCollection> {
+ val rootDirectory = virtualFile?.let { RegionFileSystem.INSTANCE.getRootByLocal(it) }
+ val project = project
+ if (project != null && rootDirectory != null) {
+ val psiRootDirectory = PsiManager.getInstance(project).findDirectory(rootDirectory)
+ if (psiRootDirectory != null) {
+ return psiRootDirectory
+ .children
+ .asSequence()
+ .mapNotNull { it as? PsiFile }
+ .map { PsiFileNode(it.project, it, settings) }
+ .toMutableList()
+ }
+ }
+
+ return ContainerUtil.emptyList()
+ }
+}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index febf4e3fc..ba36bcbbd 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -339,6 +339,7 @@
+
@@ -412,6 +413,11 @@
+
+
+
+
+