diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt index c6f01bf7..7feadf32 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt @@ -23,6 +23,7 @@ package com.falsepattern.zigbrains.debugger.toolchain import com.falsepattern.zigbrains.debugger.ZigDebugBundle +import com.falsepattern.zigbrains.shared.Unarchiver import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.application.PathManager @@ -40,14 +41,12 @@ import com.intellij.ui.components.JBPanel import com.intellij.util.application import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.download.DownloadableFileService -import com.intellij.util.io.Decompressor import com.intellij.util.system.CpuArch import com.intellij.util.system.OS import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager import com.jetbrains.cidr.execution.debugger.backend.bin.UrlProvider import com.jetbrains.cidr.execution.debugger.backend.lldb.LLDBDriverConfiguration import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import java.io.IOException import java.net.URL @@ -329,38 +328,6 @@ class ZigDebuggerToolchainService { } } - private enum class Unarchiver { - ZIP { - override val extension = "zip" - override fun createDecompressor(file: Path) = Decompressor.Zip(file) - }, - TAR { - override val extension = "tar.gz" - override fun createDecompressor(file: Path) = Decompressor.Tar(file) - }, - VSIX { - override val extension = "vsix" - override fun createDecompressor(file: Path) = Decompressor.Zip(file) - }; - - protected abstract val extension: String - protected abstract fun createDecompressor(file: Path): Decompressor - - companion object { - @Throws(IOException::class) - suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { - runInterruptible { - val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } ?: error("Unexpected archive type: $archivePath") - val dec = unarchiver.createDecompressor(archivePath) - if (prefix != null) { - dec.removePrefixPath(prefix) - } - dec.extract(dst) - } - } - } - } - sealed class DownloadResult { class Ok(val baseDir: Path): DownloadResult() data object NoUrls: DownloadResult() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index d12c54ce..740f551a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.resolve import com.falsepattern.zigbrains.project.toolchain.base.toRef import com.intellij.openapi.components.* +import java.lang.ref.WeakReference import java.util.UUID @Service(Service.Level.APP) @@ -34,13 +35,15 @@ import java.util.UUID storages = [Storage("zigbrains.xml")] ) class ZigToolchainListService: SerializablePersistentStateComponent(State()) { + private val changeListeners = ArrayList>() fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { updateState { val newMap = HashMap() newMap.putAll(it.toolchains) - newMap.put(uuid.toString(), toolchain.toRef()) + newMap[uuid.toString()] = toolchain.toRef() it.copy(toolchains = newMap) } + notifyChanged() } fun getToolchain(uuid: UUID): ZigToolchain? { @@ -52,6 +55,37 @@ class ZigToolchainListService: SerializablePersistentStateComponent> @@ -72,4 +106,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent> { - return withProgressText("Fetching zig version information") { - withContext(Dispatchers.IO) { - doDownload() - } +data class ZigVersionInfo( + val version: SemVer, + val date: String, + val docs: String, + val notes: String, + val src: Tarball?, + val dist: Tarball +) { + suspend fun downloadAndUnpack(into: Path): Boolean { + return reportProgress { reporter -> + try { + into.createDirectories() + } catch (e: Exception) { + return@reportProgress false } - } - - @OptIn(ExperimentalSerializationApi::class) - private suspend fun doDownload(): List> { val service = DownloadableFileService.getInstance() - val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json") + val fileName = dist.tarball.substringAfterLast('/') + val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) + val desc = service.createFileDescription(dist.tarball, tempFile.name) val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") - val downloadDirectory = tempPluginDir.toFile() - val downloadResults = coroutineToIndicator { - downloader.download(downloadDirectory) - } - var info: JsonObject? = null - for (result in downloadResults) { - if (result.second.defaultFileName == "index.json") { - info = result.first.inputStream().use { Json.decodeFromStream(it) } + val downloadResults = reporter.sizedStep(100) { + coroutineToIndicator { + downloader.download(into.toFile()) } } - return info?.mapNotNull { (version, data) -> parseVersion(data)?.let { Pair(version, it) } }?.toList() ?: emptyList() + if (downloadResults.isEmpty()) + return@reportProgress false + val tarball = downloadResults[0].first + reporter.indeterminateStep("Extracting tarball") { + Unarchiver.unarchive(tarball.toPath(), into) + tarball.delete() + val contents = Files.newDirectoryStream(into).use { it.toList() } + if (contents.size == 1 && contents[0].isDirectory()) { + val src = contents[0] + Files.newDirectoryStream(src).use { stream -> + stream.forEach { + it.move(into.resolve(src.relativize(it))) + } + } + src.delete() + } + } + return@reportProgress true + } + } + companion object { + @OptIn(ExperimentalSerializationApi::class) + suspend fun downloadVersionList(): List { + val service = DownloadableFileService.getInstance() + val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false) + val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name) + val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") + val downloadResults = coroutineToIndicator { + downloader.download(tempPluginDir) + } + if (downloadResults.isEmpty()) + return emptyList() + val index = downloadResults[0].first + val info = index.inputStream().use { Json.decodeFromStream(it) } + index.delete() + return info.mapNotNull { (version, data) -> parseVersion(version, data) }.toList() } - private fun parseVersion(data: JsonElement): ZigVersionInfo? { - data as? JsonObject ?: return null + private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? { + if (data !is JsonObject) + return null + + val versionTag = data["version"]?.asSafely()?.content + + val version = SemVer.parseFromText(versionTag) ?: SemVer.parseFromText(versionKey) + ?: return null val date = data["date"]?.asSafely()?.content ?: "" val docs = data["docs"]?.asSafely()?.content ?: "" - val notes = data["notes"]?.asSafely()?.content ?: "" + val notes = data["notes"]?.asSafely()?.content?: "" val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } - val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } ?: return null + val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } + ?: return null - return ZigVersionInfo(date, docs, notes, src, dist) + + return ZigVersionInfo(version, date, docs, notes, src, dist) } private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { @@ -112,4 +164,4 @@ data class ZigVersionInfo(val date: String, val docs: String, val notes: String, @Serializable data class Tarball(val tarball: String, val shasum: String, val size: Int) -private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains") \ No newline at end of file +private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt index ba80d0f7..f7770eb8 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt @@ -24,64 +24,200 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.intellij.openapi.application.ModalityState +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT +import com.intellij.icons.AllIcons import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.TaskCancellation import com.intellij.platform.ide.progress.withModalProgress +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBLabel import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.panel +import com.intellij.util.asSafely +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.awt.Component +import java.nio.file.Files +import java.nio.file.Path +import java.util.UUID import javax.swing.DefaultComboBoxModel +import javax.swing.JList +import javax.swing.event.DocumentEvent +import kotlin.contracts.ExperimentalContracts +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +//TODO lang object Downloader { - suspend fun openDownloadDialog(component: Component) { + suspend fun downloadToolchain(component: Component): UUID? { val info = withModalProgress( - component.let { ModalTaskOwner.component(it) }, + ModalTaskOwner.component(component), "Fetching zig version information", - TaskCancellation.Companion.cancellable()) { - ZigVersionInfo.Companion.download() + TaskCancellation.cancellable()) { + withContext(Dispatchers.IO) { + ZigVersionInfo.downloadVersionList() + } } - withEDTContext(ModalityState.stateForComponent(component)) { - val dialog = DialogBuilder() - val theList = ComboBox(DefaultComboBoxModel(info.map { it.first }.toTypedArray())) - val outputPath = textFieldWithBrowseButton( - null, - FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) - ).also { - Disposer.register(dialog, it) + val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) { + selectToolchain(info) + } ?: return null + withModalProgress( + ModalTaskOwner.component(component), + "Downloading zig tarball", + TaskCancellation.cancellable()) { + withContext(Dispatchers.IO) { + version.downloadAndUnpack(downloadPath) } - var archiveSizeCell: Cell<*>? = null - fun detect(item: String) { - outputPath.text = System.getProperty("user.home") + "/.zig/" + item - val data = info.firstOrNull { it.first == item } ?: return - val size = data.second.dist.size - val sizeMb = size / (1024f * 1024f) - archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) + } + return null + } + + private enum class DirectoryState { + Invalid, + NotAbsolute, + NotDirectory, + NotEmpty, + CreateNew, + Ok; + + fun isValid(): Boolean { + return when(this) { + Invalid, NotAbsolute, NotDirectory, NotEmpty -> false + CreateNew, Ok -> true } - theList.addItemListener { - detect(it.item as String) - } - val center = panel { - row("Version:") { - cell(theList).resizableColumn().align(AlignX.FILL) + } + + companion object { + @OptIn(ExperimentalContracts::class) + @JvmStatic + fun determine(path: Path?): DirectoryState { + if (path == null) { + return Invalid } - row("Location:") { - cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } + if (!path.isAbsolute) { + return NotAbsolute } + if (!path.exists()) { + var parent: Path? = path.parent + while(parent != null) { + if (!parent.exists()) { + parent = parent.parent + continue + } + if (!parent.isDirectory()) { + return NotDirectory + } + return CreateNew + } + return Invalid + } + if (!path.isDirectory()) { + return NotDirectory + } + val isEmpty = Files.newDirectoryStream(path).use { !it.iterator().hasNext() } + if (!isEmpty) { + return NotEmpty + } + return Ok } - detect(info[0].first) - dialog.centerPanel(center) - dialog.setTitle("Version Selector") - dialog.addCancelAction() - dialog.showAndGet() - Disposer.dispose(dialog) } } + + private fun selectToolchain(info: List): Pair? { + val dialog = DialogBuilder() + val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray())) + theList.renderer = object: ColoredListCellRenderer() { + override fun customizeCellRenderer( + list: JList, + value: ZigVersionInfo?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + value?.let { append(it.version.rawVersion) } + } + } + val outputPath = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + ) + Disposer.register(dialog, outputPath) + outputPath.textField.columns = 50 + + lateinit var errorMessageBox: JBLabel + outputPath.addDocumentListener(object: DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() + val state = DirectoryState.determine(path) + if (state.isValid()) { + errorMessageBox.icon = AllIcons.General.Information + dialog.setOkActionEnabled(true) + } else { + errorMessageBox.icon = AllIcons.General.Error + dialog.setOkActionEnabled(false) + } + errorMessageBox.text = when(state) { + DirectoryState.Invalid -> "Invalid path" + DirectoryState.NotAbsolute -> "Must be an absolute path" + DirectoryState.NotDirectory -> "Path is not a directory" + DirectoryState.NotEmpty -> "Directory is not empty" + DirectoryState.CreateNew -> "Directory will be created" + DirectoryState.Ok -> "Directory OK" + } + dialog.window.repaint() + } + }) + var archiveSizeCell: Cell<*>? = null + fun detect(item: ZigVersionInfo) { + outputPath.text = System.getProperty("user.home") + "/.zig/" + item.version + val size = item.dist.size + val sizeMb = size / (1024f * 1024f) + archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) + } + theList.addItemListener { + detect(it.item as ZigVersionInfo) + } + val center = panel { + row("Version:") { + cell(theList).resizableColumn().align(AlignX.FILL) + } + row("Location:") { + cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } + } + row { + errorMessageBox = JBLabel() + cell(errorMessageBox) + } + } + detect(info[0]) + dialog.centerPanel(center) + dialog.setTitle("Version Selector") + dialog.addCancelAction() + dialog.addOkAction().also { it.setText("Download") } + if (!dialog.showAndGet()) { + return null + } + val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() + ?: return null + if (!DirectoryState.determine(path).isValid()) { + return null + } + val version = theList.selectedItem?.asSafely() + ?: return null + + return path to version + } + + private suspend fun installToolchain(path: Path, version: ZigVersionInfo): Boolean { + TODO("Not yet implemented") + } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt index c8de5969..e83c00e1 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -72,19 +72,15 @@ class ZigToolchainEditor(private val project: Project): Configurable { super.disposeUIResources() } - inner class UI(): Disposable { + inner class UI(): Disposable, ZigToolchainListService.ToolchainListChangeListener { private val toolchainBox: TCComboBox private var oldSelectionIndex: Int = 0 + private val model: TCModel init { - val modelList = ArrayList() - modelList.add(TCListElem.None) - modelList.addAll(ZigToolchainListService.Companion.getInstance().toolchains.map { it.asActual() }) - modelList.add(Separator("", true)) - modelList.addAll(TCListElem.fetchGroup) - modelList.add(Separator("Detected toolchains", true)) - modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) - toolchainBox = TCComboBox(TCModel(modelList)) + model = TCModel(getModelList()) + toolchainBox = TCComboBox(model) toolchainBox.addItemListener(::itemStateChanged) + ZigToolchainListService.getInstance().addChangeListener(this) reset() } @@ -107,6 +103,16 @@ class ZigToolchainEditor(private val project: Project): Configurable { } } + override fun toolchainListChanged() { + val selected = model.selected + val list = getModelList() + model.updateContents(list) + if (selected != null && list.contains(selected)) { + model.selectedItem = selected + } else { + model.selectedItem = TCListElem.None + } + } fun attach(p: Panel): Unit = with(p) { row("Toolchain") { @@ -115,20 +121,32 @@ class ZigToolchainEditor(private val project: Project): Configurable { } fun isModified(): Boolean { - return ZigToolchainService.Companion.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain + return ZigToolchainService.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain } fun apply() { - ZigToolchainService.Companion.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain + ZigToolchainService.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain } fun reset() { - toolchainBox.selectedToolchain = ZigToolchainService.Companion.getInstance(project).toolchainUUID + toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID oldSelectionIndex = toolchainBox.selectedIndex } override fun dispose() { - + ZigToolchainListService.getInstance().removeChangeListener(this) } } +} + + +private fun getModelList(): List { + val modelList = ArrayList() + modelList.add(TCListElem.None) + modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }) + modelList.add(Separator("", true)) + modelList.addAll(TCListElem.fetchGroup) + modelList.add(Separator("Detected toolchains", true)) + modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) + return modelList } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index 908acb78..0d202210 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -27,6 +27,7 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -34,23 +35,25 @@ import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.util.IconUtil -import kotlinx.coroutines.async +import kotlinx.coroutines.launch import java.util.UUID import javax.swing.JComponent import javax.swing.tree.DefaultTreeModel -class ZigToolchainListEditor() : MasterDetailsComponent() { +class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener { private var isTreeInitialized = false - private var myComponent: JComponent? = null + private var registered: Boolean = false override fun createComponent(): JComponent { if (!isTreeInitialized) { initTree() isTreeInitialized = true } - val comp = super.createComponent() - myComponent = comp - return comp + if (!registered) { + ZigToolchainListService.getInstance().addChangeListener(this) + registered = true + } + return super.createComponent() } override fun createActions(fromPopup: Boolean): List { @@ -81,12 +84,10 @@ class ZigToolchainListEditor() : MasterDetailsComponent() { is TCListElem.Toolchain -> { val uuid = UUID.randomUUID() ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain) - addToolchain(uuid, elem.toolchain) - (myTree.model as DefaultTreeModel).reload() } is TCListElem.Download -> { - zigCoroutineScope.async { - Downloader.openDownloadDialog(myComponent!!) + zigCoroutineScope.launch(myWholePanel.asContextElement()) { + Downloader.downloadToolchain(myWholePanel) } } is TCListElem.FromDisk -> {} @@ -118,6 +119,12 @@ class ZigToolchainListEditor() : MasterDetailsComponent() { override fun disposeUIResources() { super.disposeUIResources() - myComponent = null + if (registered) { + ZigToolchainListService.getInstance().removeChangeListener(this) + } + } + + override fun toolchainListChanged() { + reloadTree() } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index 7322e741..e3a56704 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -85,9 +85,15 @@ internal class TCComboBox(model: TCModel): ComboBox(model) { } } -internal class TCModel private constructor(elements: List, private val separators: Map) : CollectionComboBoxModel(elements) { +internal class TCModel private constructor(elements: List, private var separators: Map) : CollectionComboBoxModel(elements) { companion object { operator fun invoke(input: List): TCModel { + val (elements, separators) = convert(input) + val model = TCModel(elements, separators) + return model + } + + private fun convert(input: List): Pair, Map> { val separators = IdentityHashMap() var lastSeparator: Separator? = null val elements = ArrayList() @@ -104,12 +110,17 @@ internal class TCModel private constructor(elements: List, private v is Separator -> lastSeparator = it } } - val model = TCModel(elements, separators) - return model + return elements to separators } } fun separatorAbove(elem: TCListElem) = separators[elem] + + fun updateContents(input: List) { + val (elements, separators) = convert(input) + this.separators = separators + replaceAll(elements) + } } internal class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt new file mode 100644 index 00000000..578567f1 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt @@ -0,0 +1,66 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains 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, only version 3 of the License. + * + * ZigBrains 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 Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +import com.intellij.util.io.Decompressor +import kotlinx.coroutines.runInterruptible +import java.io.IOException +import java.nio.file.Path +import kotlin.io.path.name + +enum class Unarchiver { + ZIP { + override val extension = "zip" + override fun createDecompressor(file: Path) = Decompressor.Zip(file) + }, + TAR_GZ { + override val extension = "tar.gz" + override fun createDecompressor(file: Path) = Decompressor.Tar(file) + }, + TAR_XZ { + override val extension = "tar.xz" + override fun createDecompressor(file: Path) = Decompressor.Tar(file) + }, + VSIX { + override val extension = "vsix" + override fun createDecompressor(file: Path) = Decompressor.Zip(file) + }; + + protected abstract val extension: String + protected abstract fun createDecompressor(file: Path): Decompressor + + companion object { + @Throws(IOException::class) + suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { + runInterruptible { + val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } + ?: error("Unexpected archive type: $archivePath") + val dec = unarchiver.createDecompressor(archivePath) + if (prefix != null) { + dec.removePrefixPath(prefix) + } + dec.extract(dst) + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt index 7a8e4c24..8f5d76b4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt @@ -30,6 +30,8 @@ import com.intellij.platform.ide.progress.TaskCancellation import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.util.application import kotlinx.coroutines.* +import java.awt.Component +import kotlin.coroutines.CoroutineContext inline fun runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleFactory: () -> String, cancellationFactory: () -> TaskCancellation = {TaskCancellation.cancellable()}, noinline action: suspend CoroutineScope.() -> T): T { return if (application.isDispatchThread) { @@ -40,7 +42,11 @@ inline fun runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleF } suspend inline fun withEDTContext(state: ModalityState, noinline block: suspend CoroutineScope.() -> T): T { - return withContext(Dispatchers.EDT + state.asContextElement(), block = block) + return withEDTContext(state.asContextElement(), block = block) +} + +suspend inline fun withEDTContext(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T): T { + return withContext(Dispatchers.EDT + context, block = block) } suspend inline fun withCurrentEDTModalityContext(noinline block: suspend CoroutineScope.() -> T): T { @@ -49,16 +55,17 @@ suspend inline fun withCurrentEDTModalityContext(noinline block: suspend Cor } } -fun T.letBlocking(targetAction: suspend CoroutineScope.(T) -> R): R { - return runBlocking { - targetAction(this@letBlocking) - } -} - suspend inline fun runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T { - return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction) + return runInterruptibleEDT(state.asContextElement(), targetAction = targetAction) +} +suspend inline fun runInterruptibleEDT(context: CoroutineContext, noinline targetAction: () -> T): T { + return runInterruptible(Dispatchers.EDT + context, block = targetAction) } fun CoroutineScope.launchWithEDT(state: ModalityState, block: suspend CoroutineScope.() -> Unit): Job { return launch(Dispatchers.EDT + state.asContextElement(), block = block) +} + +fun Component.asContextElement(): CoroutineContext { + return ModalityState.stateForComponent(this).asContextElement() } \ No newline at end of file