fully functional selection logic

This commit is contained in:
FalsePattern 2025-04-08 00:36:44 +02:00
parent 9676b70821
commit ee5a2463b9
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
14 changed files with 618 additions and 264 deletions

View file

@ -35,6 +35,7 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.util.progress.reportSequentialProgress
import com.intellij.ui.BrowserHyperlinkListener import com.intellij.ui.BrowserHyperlinkListener
import com.intellij.ui.HyperlinkLabel import com.intellij.ui.HyperlinkLabel
import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBPanel
@ -47,6 +48,7 @@ import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager
import com.jetbrains.cidr.execution.debugger.backend.bin.UrlProvider import com.jetbrains.cidr.execution.debugger.backend.bin.UrlProvider
import com.jetbrains.cidr.execution.debugger.backend.lldb.LLDBDriverConfiguration import com.jetbrains.cidr.execution.debugger.backend.lldb.LLDBDriverConfiguration
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
import java.net.URL import java.net.URL
@ -167,7 +169,9 @@ class ZigDebuggerToolchainService {
} }
try { try {
withContext(Dispatchers.IO) {
downloadAndUnArchive(baseDir, downloadableBinaries) downloadAndUnArchive(baseDir, downloadableBinaries)
}
return DownloadResult.Ok(baseDir) return DownloadResult.Ok(baseDir)
} catch (e: IOException) { } catch (e: IOException) {
//TODO logging //TODO logging
@ -206,6 +210,7 @@ class ZigDebuggerToolchainService {
@Throws(IOException::class) @Throws(IOException::class)
@RequiresEdt @RequiresEdt
private suspend fun downloadAndUnArchive(baseDir: Path, binariesToDownload: List<DownloadableDebuggerBinary>) { private suspend fun downloadAndUnArchive(baseDir: Path, binariesToDownload: List<DownloadableDebuggerBinary>) {
reportSequentialProgress { reporter ->
val service = DownloadableFileService.getInstance() val service = DownloadableFileService.getInstance()
val downloadDir = baseDir.toFile() val downloadDir = baseDir.toFile()
@ -217,7 +222,7 @@ class ZigDebuggerToolchainService {
val downloader = service.createDownloader(descriptions, "Debugger downloading") val downloader = service.createDownloader(descriptions, "Debugger downloading")
val downloadDirectory = downloadPath().toFile() val downloadDirectory = downloadPath().toFile()
val downloadResults = withContext(Dispatchers.IO) { val downloadResults = reporter.sizedStep(100) {
coroutineToIndicator { coroutineToIndicator {
downloader.download(downloadDirectory) downloader.download(downloadDirectory)
} }
@ -228,13 +233,18 @@ class ZigDebuggerToolchainService {
val binaryToDownload = binariesToDownload.first { it.url == downloadUrl } val binaryToDownload = binariesToDownload.first { it.url == downloadUrl }
val propertyName = binaryToDownload.propertyName val propertyName = binaryToDownload.propertyName
val archiveFile = result.first val archiveFile = result.first
reporter.indeterminateStep {
coroutineToIndicator {
Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix) Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix)
}
}
archiveFile.delete() archiveFile.delete()
versions[propertyName] = binaryToDownload.version versions[propertyName] = binaryToDownload.version
} }
saveVersionsFile(baseDir, versions) saveVersionsFile(baseDir, versions)
} }
}
private fun lldbUrls(): Pair<URL, URL>? { private fun lldbUrls(): Pair<URL, URL>? {
val lldb = UrlProvider.lldb(OS.CURRENT, CpuArch.CURRENT) ?: return null val lldb = UrlProvider.lldb(OS.CURRENT, CpuArch.CURRENT) ?: return null

View file

@ -25,7 +25,9 @@ package com.falsepattern.zigbrains.project.toolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.resolve import com.falsepattern.zigbrains.project.toolchain.base.resolve
import com.falsepattern.zigbrains.project.toolchain.base.toRef import com.falsepattern.zigbrains.project.toolchain.base.toRef
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.components.* import com.intellij.openapi.components.*
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.UUID import java.util.UUID
@ -37,19 +39,43 @@ import java.util.UUID
class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchainListService.State>(State()) { class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchainListService.State>(State()) {
private val changeListeners = ArrayList<WeakReference<ToolchainListChangeListener>>() private val changeListeners = ArrayList<WeakReference<ToolchainListChangeListener>>()
fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { fun setToolchain(uuid: UUID, toolchain: ZigToolchain) {
val str = uuid.toString()
val ref = toolchain.toRef()
updateState { updateState {
val newMap = HashMap<String, ZigToolchain.Ref>() val newMap = HashMap<String, ZigToolchain.Ref>()
newMap.putAll(it.toolchains) newMap.putAll(it.toolchains)
newMap[uuid.toString()] = toolchain.toRef() newMap[str] = ref
it.copy(toolchains = newMap) it.copy(toolchains = newMap)
} }
notifyChanged() notifyChanged()
} }
fun registerNewToolchain(toolchain: ZigToolchain): UUID {
val ref = toolchain.toRef()
var uuid = UUID.randomUUID()
updateState {
val newMap = HashMap<String, ZigToolchain.Ref>()
newMap.putAll(it.toolchains)
var uuidStr = uuid.toString()
while (newMap.containsKey(uuidStr)) {
uuid = UUID.randomUUID()
uuidStr = uuid.toString()
}
newMap[uuidStr] = ref
it.copy(toolchains = newMap)
}
notifyChanged()
return uuid
}
fun getToolchain(uuid: UUID): ZigToolchain? { fun getToolchain(uuid: UUID): ZigToolchain? {
return state.toolchains[uuid.toString()]?.resolve() return state.toolchains[uuid.toString()]?.resolve()
} }
fun hasToolchain(uuid: UUID): Boolean {
return state.toolchains.containsKey(uuid.toString())
}
fun removeToolchain(uuid: UUID) { fun removeToolchain(uuid: UUID) {
val str = uuid.toString() val str = uuid.toString()
updateState { updateState {
@ -67,7 +93,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
changeListeners.removeAt(i) changeListeners.removeAt(i)
continue continue
} }
zigCoroutineScope.launch {
v.toolchainListChanged() v.toolchainListChanged()
}
i++ i++
} }
} }
@ -88,6 +116,7 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
} }
} }
val toolchains: Sequence<Pair<UUID, ZigToolchain>> val toolchains: Sequence<Pair<UUID, ZigToolchain>>
get() = state.toolchains get() = state.toolchains
.asSequence() .asSequence()
@ -109,6 +138,6 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
@FunctionalInterface @FunctionalInterface
interface ToolchainListChangeListener { interface ToolchainListChangeListener {
fun toolchainListChanged() suspend fun toolchainListChanged()
} }
} }

View file

@ -39,7 +39,16 @@ import java.util.UUID
) )
class ZigToolchainService: SerializablePersistentStateComponent<ZigToolchainService.State>(State()) { class ZigToolchainService: SerializablePersistentStateComponent<ZigToolchainService.State>(State()) {
var toolchainUUID: UUID? var toolchainUUID: UUID?
get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) } get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf {
if (ZigToolchainListService.getInstance().hasToolchain(it)) {
true
} else {
updateState {
it.copy(toolchain = "")
}
false
}
}
set(value) { set(value) {
updateState { updateState {
it.copy(toolchain = value?.toString() ?: "") it.copy(toolchain = value?.toString() ?: "")
@ -49,11 +58,10 @@ class ZigToolchainService: SerializablePersistentStateComponent<ZigToolchainServ
val toolchain: ZigToolchain? val toolchain: ZigToolchain?
get() = toolchainUUID?.let { ZigToolchainListService.getInstance().getToolchain(it) } get() = toolchainUUID?.let { ZigToolchainListService.getInstance().getToolchain(it) }
@JvmRecord
data class State( data class State(
@JvmField @JvmField
@Attribute @Attribute
val toolchain: String = "" var toolchain: String = ""
) )
companion object { companion object {

View file

@ -0,0 +1,57 @@
package com.falsepattern.zigbrains.project.toolchain.downloader
import java.nio.file.Files
import java.nio.file.Path
import kotlin.contracts.ExperimentalContracts
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
enum class DirectoryState {
Invalid,
NotAbsolute,
NotDirectory,
NotEmpty,
CreateNew,
Ok;
fun isValid(): Boolean {
return when(this) {
Invalid, NotAbsolute, NotDirectory, NotEmpty -> false
CreateNew, Ok -> true
}
}
companion object {
@JvmStatic
fun determine(path: Path?): DirectoryState {
if (path == null) {
return Invalid
}
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
}
}
}

View file

@ -20,14 +20,16 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.toolchain.ui package com.falsepattern.zigbrains.project.toolchain.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT
import com.intellij.icons.AllIcons import com.intellij.icons.AllIcons
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.observable.util.whenFocusGained
import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
@ -43,95 +45,38 @@ import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.panel
import com.intellij.util.asSafely import com.intellij.util.asSafely
import kotlinx.coroutines.Dispatchers import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.withContext
import java.awt.Component import java.awt.Component
import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.UUID import java.util.*
import javax.swing.DefaultComboBoxModel import javax.swing.DefaultComboBoxModel
import javax.swing.JList import javax.swing.JList
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent
import kotlin.contracts.ExperimentalContracts
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
//TODO lang //TODO lang
object Downloader { object Downloader {
suspend fun downloadToolchain(component: Component): UUID? { suspend fun downloadToolchain(component: Component): ZigToolchain? {
val info = withModalProgress( val info = withModalProgress(
ModalTaskOwner.component(component), ModalTaskOwner.component(component),
"Fetching zig version information", "Fetching zig version information",
TaskCancellation.cancellable()) { TaskCancellation.cancellable()
withContext(Dispatchers.IO) { ) {
ZigVersionInfo.downloadVersionList() ZigVersionInfo.downloadVersionList()
} }
}
val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) { val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) {
selectToolchain(info) selectToolchain(info)
} ?: return null } ?: return null
withModalProgress( withModalProgress(
ModalTaskOwner.component(component), ModalTaskOwner.component(component),
"Downloading zig tarball", "Installing Zig ${version.version}",
TaskCancellation.cancellable()) { TaskCancellation.cancellable()
withContext(Dispatchers.IO) { ) {
version.downloadAndUnpack(downloadPath) version.downloadAndUnpack(downloadPath)
} }
} return LocalZigToolchain.tryFromPath(downloadPath)
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
}
}
companion object {
@OptIn(ExperimentalContracts::class)
@JvmStatic
fun determine(path: Path?): DirectoryState {
if (path == null) {
return Invalid
}
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
}
}
} }
@RequiresEdt
private fun selectToolchain(info: List<ZigVersionInfo>): Pair<Path, ZigVersionInfo>? { private fun selectToolchain(info: List<ZigVersionInfo>): Pair<Path, ZigVersionInfo>? {
val dialog = DialogBuilder() val dialog = DialogBuilder()
val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray())) val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray()))
@ -148,14 +93,14 @@ object Downloader {
} }
val outputPath = textFieldWithBrowseButton( val outputPath = textFieldWithBrowseButton(
null, null,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) FileChooserDescriptorFactory.createSingleFolderDescriptor()
.withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
) )
Disposer.register(dialog, outputPath) Disposer.register(dialog, outputPath)
outputPath.textField.columns = 50 outputPath.textField.columns = 50
lateinit var errorMessageBox: JBLabel lateinit var errorMessageBox: JBLabel
outputPath.addDocumentListener(object: DocumentAdapter() { fun onChanged() {
override fun textChanged(e: DocumentEvent) {
val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() val path = outputPath.text.ifBlank { null }?.toNioPathOrNull()
val state = DirectoryState.determine(path) val state = DirectoryState.determine(path)
if (state.isValid()) { if (state.isValid()) {
@ -175,6 +120,13 @@ object Downloader {
} }
dialog.window.repaint() dialog.window.repaint()
} }
outputPath.whenFocusGained {
onChanged()
}
outputPath.addDocumentListener(object: DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
onChanged()
}
}) })
var archiveSizeCell: Cell<*>? = null var archiveSizeCell: Cell<*>? = null
fun detect(item: ZigVersionInfo) { fun detect(item: ZigVersionInfo) {
@ -200,7 +152,7 @@ object Downloader {
} }
detect(info[0]) detect(info[0])
dialog.centerPanel(center) dialog.centerPanel(center)
dialog.setTitle("Version Selector") dialog.setTitle("Zig Downloader")
dialog.addCancelAction() dialog.addCancelAction()
dialog.addOkAction().also { it.setText("Download") } dialog.addOkAction().also { it.setText("Download") }
if (!dialog.showAndGet()) { if (!dialog.showAndGet()) {
@ -216,8 +168,4 @@ object Downloader {
return path to version return path to version
} }
private suspend fun installToolchain(path: Path, version: ZigVersionInfo): Boolean {
TODO("Not yet implemented")
}
} }

View file

@ -0,0 +1,104 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.project.toolchain.downloader
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
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.DialogBuilder
import com.intellij.openapi.util.Disposer
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.panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.awt.Component
import javax.swing.event.DocumentEvent
object LocalSelector {
suspend fun browseFromDisk(component: Component): ZigToolchain? {
return runInterruptibleEDT(component.asContextElement()) {
doBrowseFromDisk()
}
}
@RequiresEdt
private fun doBrowseFromDisk(): ZigToolchain? {
val dialog = DialogBuilder()
val path = textFieldWithBrowseButton(
null,
FileChooserDescriptorFactory.createSingleFolderDescriptor()
.withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
)
Disposer.register(dialog, path)
path.textField.columns = 50
lateinit var errorMessageBox: JBLabel
path.addDocumentListener(object: DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
val tc = LocalZigToolchain.tryFromPathString(path.text)
if (tc == null) {
errorMessageBox.icon = AllIcons.General.Error
errorMessageBox.text = "Invalid toolchain path"
dialog.setOkActionEnabled(false)
} else if (ZigToolchainListService
.getInstance()
.toolchains
.mapNotNull { it.second as? LocalZigToolchain }
.any { it.location == tc.location }
) {
errorMessageBox.icon = AllIcons.General.Warning
errorMessageBox.text = tc.name?.let { "Toolchain already exists as \"$it\"" } ?: "Toolchain already exists"
dialog.setOkActionEnabled(true)
} else {
errorMessageBox.icon = Icons.Zig
errorMessageBox.text = tc.name ?: "OK"
dialog.setOkActionEnabled(true)
}
}
})
val center = panel {
row("Path:") {
cell(path).resizableColumn().align(AlignX.FILL)
}
row {
errorMessageBox = JBLabel()
cell(errorMessageBox)
}
}
dialog.centerPanel(center)
dialog.setTitle("Zig Browser")
dialog.addCancelAction()
dialog.addOkAction().also { it.setText("Add") }
if (!dialog.showAndGet()) {
return null
}
return LocalZigToolchain.tryFromPathString(path.text)
}
}

View file

@ -24,12 +24,12 @@ package com.falsepattern.zigbrains.project.toolchain.downloader
import com.falsepattern.zigbrains.shared.Unarchiver import com.falsepattern.zigbrains.shared.Unarchiver
import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.PathManager
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.coroutineToIndicator import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.util.progress.reportProgress import com.intellij.platform.util.progress.*
import com.intellij.platform.util.progress.reportSequentialProgress
import com.intellij.platform.util.progress.withProgressText
import com.intellij.util.asSafely import com.intellij.util.asSafely
import com.intellij.util.download.DownloadableFileService import com.intellij.util.download.DownloadableFileService
import com.intellij.util.io.createDirectories import com.intellij.util.io.createDirectories
@ -38,6 +38,8 @@ import com.intellij.util.io.move
import com.intellij.util.system.CpuArch import com.intellij.util.system.CpuArch
import com.intellij.util.system.OS import com.intellij.util.system.OS
import com.intellij.util.text.SemVer import com.intellij.util.text.SemVer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -47,9 +49,14 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import java.io.File import java.io.File
import java.lang.IllegalStateException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.*
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
import kotlin.io.path.name
@JvmRecord @JvmRecord
data class ZigVersionInfo( data class ZigVersionInfo(
@ -60,62 +67,105 @@ data class ZigVersionInfo(
val src: Tarball?, val src: Tarball?,
val dist: Tarball val dist: Tarball
) { ) {
suspend fun downloadAndUnpack(into: Path): Boolean { @Throws(Exception::class)
return reportProgress { reporter -> suspend fun downloadAndUnpack(into: Path) {
try { reportProgress { reporter ->
into.createDirectories() into.createDirectories()
} catch (e: Exception) { val tarball = downloadTarball(dist, into, reporter)
return@reportProgress false unpackTarball(tarball, into, reporter)
tarball.delete()
flattenDownloadDir(into, reporter)
} }
}
companion object {
@OptIn(ExperimentalSerializationApi::class)
suspend fun downloadVersionList(): List<ZigVersionInfo> {
return withContext(Dispatchers.IO) {
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")
val downloadResults = coroutineToIndicator {
downloader.download(tempPluginDir)
}
if (downloadResults.isEmpty())
return@withContext emptyList()
val index = downloadResults[0].first
val info = index.inputStream().use { Json.decodeFromStream<JsonObject>(it) }
index.delete()
return@withContext info.mapNotNull { (version, data) -> parseVersion(version, data) }.toList()
}
}
}
@JvmRecord
@Serializable
data class Tarball(val tarball: String, val shasum: String, val size: Int)
}
private suspend fun downloadTarball(dist: ZigVersionInfo.Tarball, into: Path, reporter: ProgressReporter): Path {
return withContext(Dispatchers.IO) {
val service = DownloadableFileService.getInstance() val service = DownloadableFileService.getInstance()
val fileName = dist.tarball.substringAfterLast('/') val fileName = dist.tarball.substringAfterLast('/')
val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false)
val desc = service.createFileDescription(dist.tarball, tempFile.name) val desc = service.createFileDescription(dist.tarball, tempFile.name)
val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") val downloader = service.createDownloader(listOf(desc), "Zig tarball")
val downloadResults = reporter.sizedStep(100) { val downloadResults = reporter.sizedStep(100) {
coroutineToIndicator { coroutineToIndicator {
downloader.download(into.toFile()) downloader.download(into.toFile())
} }
} }
if (downloadResults.isEmpty()) if (downloadResults.isEmpty())
return@reportProgress false throw IllegalStateException("No file downloaded")
val tarball = downloadResults[0].first return@withContext downloadResults[0].first.toPath()
reporter.indeterminateStep("Extracting tarball") { }
Unarchiver.unarchive(tarball.toPath(), into) }
tarball.delete()
val contents = Files.newDirectoryStream(into).use { it.toList() } private suspend fun flattenDownloadDir(dir: Path, reporter: ProgressReporter) {
withContext(Dispatchers.IO) {
val contents = Files.newDirectoryStream(dir).use { it.toList() }
if (contents.size == 1 && contents[0].isDirectory()) { if (contents.size == 1 && contents[0].isDirectory()) {
val src = contents[0] val src = contents[0]
reporter.indeterminateStep {
coroutineToIndicator {
val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
indicator.isIndeterminate = true
indicator.text = "Flattening directory"
Files.newDirectoryStream(src).use { stream -> Files.newDirectoryStream(src).use { stream ->
stream.forEach { stream.forEach {
it.move(into.resolve(src.relativize(it))) indicator.text2 = it.name
it.move(dir.resolve(src.relativize(it)))
}
}
} }
} }
src.delete() src.delete()
} }
} }
return@reportProgress true }
}
}
companion object {
@OptIn(ExperimentalSerializationApi::class)
suspend fun downloadVersionList(): List<ZigVersionInfo> {
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<JsonObject>(it) }
index.delete()
return info.mapNotNull { (version, data) -> parseVersion(version, data) }.toList()
}
private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? { @OptIn(ExperimentalPathApi::class)
private suspend fun unpackTarball(tarball: Path, into: Path, reporter: ProgressReporter) {
withContext(Dispatchers.IO) {
try {
reporter.indeterminateStep {
coroutineToIndicator {
Unarchiver.unarchive(tarball, into)
}
}
} catch (e: Throwable) {
tarball.delete()
val contents = Files.newDirectoryStream(into).use { it.toList() }
if (contents.size == 1 && contents[0].isDirectory()) {
contents[0].deleteRecursively()
}
throw e
}
}
}
private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? {
if (data !is JsonObject) if (data !is JsonObject)
return null return null
@ -125,16 +175,16 @@ data class ZigVersionInfo(
?: return null ?: return null
val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: "" val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: ""
val docs = data["docs"]?.asSafely<JsonPrimitive>()?.content ?: "" val docs = data["docs"]?.asSafely<JsonPrimitive>()?.content ?: ""
val notes = data["notes"]?.asSafely<JsonPrimitive>()?.content?: "" val notes = data["notes"]?.asSafely<JsonPrimitive>()?.content ?: ""
val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<Tarball>(it) } val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<ZigVersionInfo.Tarball>(it) }
val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) }
?: return null ?: return null
return ZigVersionInfo(version, date, docs, notes, src, dist) return ZigVersionInfo(version, date, docs, notes, src, dist)
} }
private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { private fun getTarballIfCompatible(dist: String, tb: JsonElement): ZigVersionInfo.Tarball? {
if (!dist.contains('-')) if (!dist.contains('-'))
return null return null
val (arch, os) = dist.split('-', limit = 2) val (arch, os) = dist.split('-', limit = 2)
@ -155,13 +205,7 @@ data class ZigVersionInfo(
if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) { if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) {
return null return null
} }
return Json.decodeFromJsonElement<Tarball>(tb) return Json.decodeFromJsonElement<ZigVersionInfo.Tarball>(tb)
}
}
} }
@JvmRecord
@Serializable
data class Tarball(val tarball: String, val shasum: String, val size: Int)
private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile() private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile()

View file

@ -65,8 +65,8 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override
} }
} }
fun tryFromPathString(pathStr: String): LocalZigToolchain? { fun tryFromPathString(pathStr: String?): LocalZigToolchain? {
return pathStr.toNioPathOrNull()?.let(::tryFromPath) return pathStr?.ifBlank { null }?.toNioPathOrNull()?.let(::tryFromPath)
} }
fun tryFromPath(path: Path): LocalZigToolchain? { fun tryFromPath(path: Path): LocalZigToolchain? {

View file

@ -0,0 +1,39 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader
import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import java.awt.Component
import java.util.UUID
internal object ZigToolchainComboBoxHandler {
@RequiresBackgroundThread
suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) {
is TCListElem.Toolchain.Suggested -> elem.toolchain
is TCListElem.Download -> Downloader.downloadToolchain(context)
is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context)
}?.let { ZigToolchainListService.getInstance().registerNewToolchain(it) }
}

View file

@ -25,15 +25,33 @@ package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.options.newEditor.SettingsDialog
import com.intellij.openapi.options.newEditor.SettingsTreeView
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.awt.event.ItemEvent import java.awt.event.ItemEvent
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.util.UUID
import javax.swing.JComponent import javax.swing.JComponent
import kotlin.collections.addAll import kotlin.collections.addAll
@ -74,7 +92,7 @@ class ZigToolchainEditor(private val project: Project): Configurable {
inner class UI(): Disposable, ZigToolchainListService.ToolchainListChangeListener { inner class UI(): Disposable, ZigToolchainListService.ToolchainListChangeListener {
private val toolchainBox: TCComboBox private val toolchainBox: TCComboBox
private var oldSelectionIndex: Int = 0 private var selectOnNextReload: UUID? = null
private val model: TCModel private val model: TCModel
init { init {
model = TCModel(getModelList()) model = TCModel(getModelList())
@ -89,27 +107,44 @@ class ZigToolchainEditor(private val project: Project): Configurable {
return return
} }
val item = event.item val item = event.item
if (item !is TCListElem) { if (item !is TCListElem.Pseudo)
toolchainBox.selectedIndex = oldSelectionIndex
return return
} zigCoroutineScope.launch(toolchainBox.asContextElement()) {
when(item) { val uuid = ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item)
is TCListElem.None, is TCListElem.Toolchain.Actual -> { withEDTContext(toolchainBox.asContextElement()) {
oldSelectionIndex = toolchainBox.selectedIndex applyUUIDNowOrOnReload(uuid)
}
else -> {
toolchainBox.selectedIndex = oldSelectionIndex
} }
} }
} }
override fun toolchainListChanged() { override suspend fun toolchainListChanged() {
val selected = model.selected withContext(Dispatchers.EDT + toolchainBox.asContextElement()) {
val list = getModelList() val list = getModelList()
model.updateContents(list) model.updateContents(list)
val onReload = selectOnNextReload
selectOnNextReload = null
if (onReload != null) {
val element = list.firstOrNull { when(it) {
is TCListElem.Toolchain.Actual -> it.uuid == onReload
else -> false
} }
model.selectedItem = element
return@withContext
}
val selected = model.selected
if (selected != null && list.contains(selected)) { if (selected != null && list.contains(selected)) {
model.selectedItem = selected model.selectedItem = selected
} else { return@withContext
}
if (selected is TCListElem.Toolchain.Actual) {
val uuid = selected.uuid
val element = list.firstOrNull { when(it) {
is TCListElem.Toolchain.Actual -> it.uuid == uuid
else -> false
} }
model.selectedItem = element
return@withContext
}
model.selectedItem = TCListElem.None model.selectedItem = TCListElem.None
} }
} }
@ -117,6 +152,35 @@ class ZigToolchainEditor(private val project: Project): Configurable {
fun attach(p: Panel): Unit = with(p) { fun attach(p: Panel): Unit = with(p) {
row("Toolchain") { row("Toolchain") {
cell(toolchainBox).resizableColumn().align(AlignX.FILL) cell(toolchainBox).resizableColumn().align(AlignX.FILL)
button("Funny") { e ->
zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) {
val config = ZigToolchainListEditor()
var inited = false
var selectedUUID: UUID? = toolchainBox.selectedToolchain
config.addItemSelectedListener {
if (inited) {
selectedUUID = it
}
}
val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config) {
config.selectNodeInTree(selectedUUID)
inited = true
}
if (apply) {
applyUUIDNowOrOnReload(selectedUUID)
}
}
}
}
}
@RequiresEdt
private fun applyUUIDNowOrOnReload(uuid: UUID?) {
toolchainBox.selectedToolchain = uuid
if (uuid != null && toolchainBox.selectedToolchain == null) {
selectOnNextReload = uuid
} else {
selectOnNextReload = null
} }
} }
@ -130,9 +194,10 @@ class ZigToolchainEditor(private val project: Project): Configurable {
fun reset() { fun reset() {
toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID
oldSelectionIndex = toolchainBox.selectedIndex
} }
override fun dispose() { override fun dispose() {
ZigToolchainListService.getInstance().removeChangeListener(this) ZigToolchainListService.getInstance().removeChangeListener(this)
} }

View file

@ -22,27 +22,56 @@
package com.falsepattern.zigbrains.project.toolchain.ui package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.Service
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.openapi.ui.MasterDetailsComponent
import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull
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.panel
import com.intellij.util.Consumer
import com.intellij.util.IconUtil import com.intellij.util.IconUtil
import com.intellij.util.asSafely
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID import java.util.UUID
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.event.DocumentEvent
import javax.swing.tree.DefaultTreeModel import javax.swing.tree.DefaultTreeModel
class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener { class ZigToolchainListEditor : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener {
private var isTreeInitialized = false private var isTreeInitialized = false
private var registered: Boolean = false private var registered: Boolean = false
private var itemSelectedListeners = ArrayList<Consumer<UUID?>>()
fun addItemSelectedListener(c: Consumer<UUID?>) {
synchronized(itemSelectedListeners) {
itemSelectedListeners.add(c)
}
}
override fun createComponent(): JComponent { override fun createComponent(): JComponent {
if (!isTreeInitialized) { if (!isTreeInitialized) {
@ -72,6 +101,14 @@ class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListServi
return listOf(add, MyDeleteAction()) return listOf(add, MyDeleteAction())
} }
override fun updateSelection(configurable: NamedConfigurable<*>?) {
super.updateSelection(configurable)
val uuid = configurable?.editableObject as? UUID
synchronized(itemSelectedListeners) {
itemSelectedListeners.forEach { it.consume(uuid) }
}
}
override fun onItemDeleted(item: Any?) { override fun onItemDeleted(item: Any?) {
if (item is UUID) { if (item is UUID) {
ZigToolchainListService.getInstance().removeToolchain(item) ZigToolchainListService.getInstance().removeToolchain(item)
@ -80,18 +117,15 @@ class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListServi
} }
private fun onItemSelected(elem: TCListElem) { private fun onItemSelected(elem: TCListElem) {
when (elem) { if (elem !is TCListElem.Pseudo)
is TCListElem.Toolchain -> { return
val uuid = UUID.randomUUID()
ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain)
}
is TCListElem.Download -> {
zigCoroutineScope.launch(myWholePanel.asContextElement()) { zigCoroutineScope.launch(myWholePanel.asContextElement()) {
Downloader.downloadToolchain(myWholePanel) val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem)
if (uuid != null) {
withEDTContext(myWholePanel.asContextElement()) {
selectNodeInTree(uuid)
} }
} }
is TCListElem.FromDisk -> {}
is TCListElem.None -> {}
} }
} }
@ -110,11 +144,15 @@ class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListServi
} }
private fun reloadTree() { private fun reloadTree() {
val currentSelection = selectedObject?.asSafely<UUID>()
myRoot.removeAllChildren() myRoot.removeAllChildren()
ZigToolchainListService.getInstance().toolchains.forEach { (uuid, toolchain) -> ZigToolchainListService.getInstance().toolchains.forEach { (uuid, toolchain) ->
addToolchain(uuid, toolchain) addToolchain(uuid, toolchain)
} }
(myTree.model as DefaultTreeModel).reload() (myTree.model as DefaultTreeModel).reload()
currentSelection?.let {
selectNodeInTree(it)
}
} }
override fun disposeUIResources() { override fun disposeUIResources() {
@ -124,7 +162,9 @@ class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListServi
} }
} }
override fun toolchainListChanged() { override suspend fun toolchainListChanged() {
withEDTContext(myWholePanel.asContextElement()) {
reloadTree() reloadTree()
} }
}
} }

View file

@ -27,20 +27,20 @@ import java.util.UUID
internal sealed interface TCListElemIn internal sealed interface TCListElemIn
internal sealed interface TCListElem : TCListElemIn { internal sealed interface TCListElem : TCListElemIn {
sealed interface Pseudo: TCListElem
sealed interface Toolchain : TCListElem { sealed interface Toolchain : TCListElem {
val toolchain: ZigToolchain val toolchain: ZigToolchain
@JvmRecord @JvmRecord
data class Suggested(override val toolchain: ZigToolchain): Toolchain data class Suggested(override val toolchain: ZigToolchain): Toolchain, Pseudo
@JvmRecord @JvmRecord
data class Actual(val uuid: UUID, override val toolchain: ZigToolchain): Toolchain data class Actual(val uuid: UUID, override val toolchain: ZigToolchain): Toolchain
} }
object None: TCListElem object None: TCListElem
object Download : TCListElem object Download : TCListElem, Pseudo
object FromDisk : TCListElem object FromDisk : TCListElem, Pseudo
companion object { companion object {
val fetchGroup get() = listOf(Download, FromDisk) val fetchGroup get() = listOf(Download, FromDisk)

View file

@ -22,8 +22,9 @@
package com.falsepattern.zigbrains.shared package com.falsepattern.zigbrains.shared
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.util.io.Decompressor import com.intellij.util.io.Decompressor
import kotlinx.coroutines.runInterruptible
import java.io.IOException import java.io.IOException
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.name import kotlin.io.path.name
@ -51,16 +52,22 @@ enum class Unarchiver {
companion object { companion object {
@Throws(IOException::class) @Throws(IOException::class)
suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) {
runInterruptible {
val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } val unarchiver = entries.find { archivePath.name.endsWith(it.extension) }
?: error("Unexpected archive type: $archivePath") ?: error("Unexpected archive type: $archivePath")
val dec = unarchiver.createDecompressor(archivePath) val dec = unarchiver.createDecompressor(archivePath)
val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
indicator.isIndeterminate = true
indicator.text = "Extracting archive"
dec.filter {
indicator.text2 = it
indicator.checkCanceled()
true
}
if (prefix != null) { if (prefix != null) {
dec.removePrefixPath(prefix) dec.removePrefixPath(prefix)
} }
dec.extract(dst) dec.extract(dst)
} }
} }
}
} }

View file

@ -63,7 +63,10 @@ suspend inline fun <T> runInterruptibleEDT(context: CoroutineContext, noinline t
} }
fun CoroutineScope.launchWithEDT(state: ModalityState, block: suspend CoroutineScope.() -> Unit): Job { fun CoroutineScope.launchWithEDT(state: ModalityState, block: suspend CoroutineScope.() -> Unit): Job {
return launch(Dispatchers.EDT + state.asContextElement(), block = block) return launchWithEDT(state.asContextElement(), block = block)
}
fun CoroutineScope.launchWithEDT(context: CoroutineContext, block: suspend CoroutineScope.() -> Unit): Job {
return launch(Dispatchers.EDT + context, block = block)
} }
fun Component.asContextElement(): CoroutineContext { fun Component.asContextElement(): CoroutineContext {