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 {
downloadAndUnArchive(baseDir, downloadableBinaries) withContext(Dispatchers.IO) {
downloadAndUnArchive(baseDir, downloadableBinaries)
}
return DownloadResult.Ok(baseDir) return DownloadResult.Ok(baseDir)
} catch (e: IOException) { } catch (e: IOException) {
//TODO logging //TODO logging
@ -206,34 +210,40 @@ 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>) {
val service = DownloadableFileService.getInstance() reportSequentialProgress { reporter ->
val service = DownloadableFileService.getInstance()
val downloadDir = baseDir.toFile() val downloadDir = baseDir.toFile()
downloadDir.deleteRecursively() downloadDir.deleteRecursively()
val descriptions = binariesToDownload.map { val descriptions = binariesToDownload.map {
service.createFileDescription(it.url, fileName(it.url)) service.createFileDescription(it.url, fileName(it.url))
}
val downloader = service.createDownloader(descriptions, "Debugger downloading")
val downloadDirectory = downloadPath().toFile()
val downloadResults = withContext(Dispatchers.IO) {
coroutineToIndicator {
downloader.download(downloadDirectory)
} }
}
val versions = Properties()
for (result in downloadResults) {
val downloadUrl = result.second.downloadUrl
val binaryToDownload = binariesToDownload.first { it.url == downloadUrl }
val propertyName = binaryToDownload.propertyName
val archiveFile = result.first
Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix)
archiveFile.delete()
versions[propertyName] = binaryToDownload.version
}
saveVersionsFile(baseDir, versions) val downloader = service.createDownloader(descriptions, "Debugger downloading")
val downloadDirectory = downloadPath().toFile()
val downloadResults = reporter.sizedStep(100) {
coroutineToIndicator {
downloader.download(downloadDirectory)
}
}
val versions = Properties()
for (result in downloadResults) {
val downloadUrl = result.second.downloadUrl
val binaryToDownload = binariesToDownload.first { it.url == downloadUrl }
val propertyName = binaryToDownload.propertyName
val archiveFile = result.first
reporter.indeterminateStep {
coroutineToIndicator {
Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix)
}
}
archiveFile.delete()
versions[propertyName] = binaryToDownload.version
}
saveVersionsFile(baseDir, versions)
}
} }
private fun lldbUrls(): Pair<URL, URL>? { private fun lldbUrls(): Pair<URL, URL>? {

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
} }
v.toolchainListChanged() zigCoroutineScope.launch {
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 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
}
} }
return LocalZigToolchain.tryFromPath(downloadPath)
} }
@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,32 +93,39 @@ 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
fun onChanged() {
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()
}
outputPath.whenFocusGained {
onChanged()
}
outputPath.addDocumentListener(object: DocumentAdapter() { outputPath.addDocumentListener(object: DocumentAdapter() {
override fun textChanged(e: DocumentEvent) { override fun textChanged(e: DocumentEvent) {
val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() onChanged()
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 var archiveSizeCell: Cell<*>? = null
@ -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,108 +67,145 @@ 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()
val service = DownloadableFileService.getInstance() flattenDownloadDir(into, reporter)
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 downloadResults = reporter.sizedStep(100) {
coroutineToIndicator {
downloader.download(into.toFile())
}
}
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 { companion object {
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
suspend fun downloadVersionList(): List<ZigVersionInfo> { suspend fun downloadVersionList(): List<ZigVersionInfo> {
val service = DownloadableFileService.getInstance() return withContext(Dispatchers.IO) {
val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false) val service = DownloadableFileService.getInstance()
val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name) val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false)
val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name)
val downloadResults = coroutineToIndicator { val downloader = service.createDownloader(listOf(desc), "Zig version information")
downloader.download(tempPluginDir) 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()
} }
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? { @JvmRecord
if (data !is JsonObject) @Serializable
return null data class Tarball(val tarball: String, val shasum: String, val size: Int)
}
val versionTag = data["version"]?.asSafely<JsonPrimitive>()?.content private suspend fun downloadTarball(dist: ZigVersionInfo.Tarball, into: Path, reporter: ProgressReporter): Path {
return withContext(Dispatchers.IO) {
val version = SemVer.parseFromText(versionTag) ?: SemVer.parseFromText(versionKey) val service = DownloadableFileService.getInstance()
?: return null val fileName = dist.tarball.substringAfterLast('/')
val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: "" val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false)
val docs = data["docs"]?.asSafely<JsonPrimitive>()?.content ?: "" val desc = service.createFileDescription(dist.tarball, tempFile.name)
val notes = data["notes"]?.asSafely<JsonPrimitive>()?.content?: "" val downloader = service.createDownloader(listOf(desc), "Zig tarball")
val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<Tarball>(it) } val downloadResults = reporter.sizedStep(100) {
val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } coroutineToIndicator {
?: return null downloader.download(into.toFile())
}
return ZigVersionInfo(version, date, docs, notes, src, dist)
} }
if (downloadResults.isEmpty())
throw IllegalStateException("No file downloaded")
return@withContext downloadResults[0].first.toPath()
}
}
private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { private suspend fun flattenDownloadDir(dir: Path, reporter: ProgressReporter) {
if (!dist.contains('-')) withContext(Dispatchers.IO) {
return null val contents = Files.newDirectoryStream(dir).use { it.toList() }
val (arch, os) = dist.split('-', limit = 2) if (contents.size == 1 && contents[0].isDirectory()) {
val theArch = when (arch) { val src = contents[0]
"x86_64" -> CpuArch.X86_64 reporter.indeterminateStep {
"i386" -> CpuArch.X86 coroutineToIndicator {
"armv7a" -> CpuArch.ARM32 val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
"aarch64" -> CpuArch.ARM64 indicator.isIndeterminate = true
else -> return null indicator.text = "Flattening directory"
Files.newDirectoryStream(src).use { stream ->
stream.forEach {
indicator.text2 = it.name
it.move(dir.resolve(src.relativize(it)))
}
}
}
} }
val theOS = when (os) { src.delete()
"linux" -> OS.Linux
"windows" -> OS.Windows
"macos" -> OS.macOS
"freebsd" -> OS.FreeBSD
else -> return null
}
if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) {
return null
}
return Json.decodeFromJsonElement<Tarball>(tb)
} }
} }
} }
@JvmRecord @OptIn(ExperimentalPathApi::class)
@Serializable private suspend fun unpackTarball(tarball: Path, into: Path, reporter: ProgressReporter) {
data class Tarball(val tarball: String, val shasum: String, val size: Int) 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)
return null
val versionTag = data["version"]?.asSafely<JsonPrimitive>()?.content
val version = SemVer.parseFromText(versionTag) ?: SemVer.parseFromText(versionKey)
?: return null
val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: ""
val docs = data["docs"]?.asSafely<JsonPrimitive>()?.content ?: ""
val notes = data["notes"]?.asSafely<JsonPrimitive>()?.content ?: ""
val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<ZigVersionInfo.Tarball>(it) }
val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) }
?: return null
return ZigVersionInfo(version, date, docs, notes, src, dist)
}
private fun getTarballIfCompatible(dist: String, tb: JsonElement): ZigVersionInfo.Tarball? {
if (!dist.contains('-'))
return null
val (arch, os) = dist.split('-', limit = 2)
val theArch = when (arch) {
"x86_64" -> CpuArch.X86_64
"i386" -> CpuArch.X86
"armv7a" -> CpuArch.ARM32
"aarch64" -> CpuArch.ARM64
else -> return null
}
val theOS = when (os) {
"linux" -> OS.Linux
"windows" -> OS.Windows
"macos" -> OS.macOS
"freebsd" -> OS.FreeBSD
else -> return null
}
if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) {
return null
}
return Json.decodeFromJsonElement<ZigVersionInfo.Tarball>(tb)
}
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)
if (selected != null && list.contains(selected)) { val onReload = selectOnNextReload
model.selectedItem = selected selectOnNextReload = null
} else { 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)) {
model.selectedItem = selected
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() zigCoroutineScope.launch(myWholePanel.asContextElement()) {
ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain) val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem)
} if (uuid != null) {
is TCListElem.Download -> { withEDTContext(myWholePanel.asContextElement()) {
zigCoroutineScope.launch(myWholePanel.asContextElement()) { selectNodeInTree(uuid)
Downloader.downloadToolchain(myWholePanel)
} }
} }
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() {
reloadTree() withEDTContext(myWholePanel.asContextElement()) {
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()
if (prefix != null) { indicator.isIndeterminate = true
dec.removePrefixPath(prefix) indicator.text = "Extracting archive"
} dec.filter {
dec.extract(dst) indicator.text2 = it
indicator.checkCanceled()
true
} }
if (prefix != null) {
dec.removePrefixPath(prefix)
}
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 {