Working change sync and downloader

This commit is contained in:
FalsePattern 2025-04-07 02:38:12 +02:00
parent 9023026478
commit 9676b70821
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
9 changed files with 437 additions and 134 deletions

View file

@ -23,6 +23,7 @@
package com.falsepattern.zigbrains.debugger.toolchain package com.falsepattern.zigbrains.debugger.toolchain
import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.debugger.ZigDebugBundle
import com.falsepattern.zigbrains.shared.Unarchiver
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.PathManager
@ -40,14 +41,12 @@ import com.intellij.ui.components.JBPanel
import com.intellij.util.application import com.intellij.util.application
import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.download.DownloadableFileService import com.intellij.util.download.DownloadableFileService
import com.intellij.util.io.Decompressor
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.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager 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
@ -329,38 +328,6 @@ class ZigDebuggerToolchainService {
} }
} }
private enum class Unarchiver {
ZIP {
override val extension = "zip"
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
},
TAR {
override val extension = "tar.gz"
override fun createDecompressor(file: Path) = Decompressor.Tar(file)
},
VSIX {
override val extension = "vsix"
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
};
protected abstract val extension: String
protected abstract fun createDecompressor(file: Path): Decompressor
companion object {
@Throws(IOException::class)
suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) {
runInterruptible {
val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } ?: error("Unexpected archive type: $archivePath")
val dec = unarchiver.createDecompressor(archivePath)
if (prefix != null) {
dec.removePrefixPath(prefix)
}
dec.extract(dst)
}
}
}
}
sealed class DownloadResult { sealed class DownloadResult {
class Ok(val baseDir: Path): DownloadResult() class Ok(val baseDir: Path): DownloadResult()
data object NoUrls: DownloadResult() data object NoUrls: DownloadResult()

View file

@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.resolve import com.falsepattern.zigbrains.project.toolchain.base.resolve
import com.falsepattern.zigbrains.project.toolchain.base.toRef import com.falsepattern.zigbrains.project.toolchain.base.toRef
import com.intellij.openapi.components.* import com.intellij.openapi.components.*
import java.lang.ref.WeakReference
import java.util.UUID import java.util.UUID
@Service(Service.Level.APP) @Service(Service.Level.APP)
@ -34,13 +35,15 @@ import java.util.UUID
storages = [Storage("zigbrains.xml")] storages = [Storage("zigbrains.xml")]
) )
class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchainListService.State>(State()) { class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchainListService.State>(State()) {
private val changeListeners = ArrayList<WeakReference<ToolchainListChangeListener>>()
fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { fun setToolchain(uuid: UUID, toolchain: ZigToolchain) {
updateState { updateState {
val newMap = HashMap<String, ZigToolchain.Ref>() val newMap = HashMap<String, ZigToolchain.Ref>()
newMap.putAll(it.toolchains) newMap.putAll(it.toolchains)
newMap.put(uuid.toString(), toolchain.toRef()) newMap[uuid.toString()] = toolchain.toRef()
it.copy(toolchains = newMap) it.copy(toolchains = newMap)
} }
notifyChanged()
} }
fun getToolchain(uuid: UUID): ZigToolchain? { fun getToolchain(uuid: UUID): ZigToolchain? {
@ -52,6 +55,37 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
updateState { updateState {
it.copy(toolchains = it.toolchains.filter { it.key != str }) it.copy(toolchains = it.toolchains.filter { it.key != str })
} }
notifyChanged()
}
private fun notifyChanged() {
synchronized(changeListeners) {
var i = 0
while (i < changeListeners.size) {
val v = changeListeners[i].get()
if (v == null) {
changeListeners.removeAt(i)
continue
}
v.toolchainListChanged()
i++
}
}
}
fun addChangeListener(listener: ToolchainListChangeListener) {
synchronized(changeListeners) {
changeListeners.add(WeakReference(listener))
}
}
fun removeChangeListener(listener: ToolchainListChangeListener) {
synchronized(changeListeners) {
changeListeners.removeIf {
val v = it.get()
v == null || v === listener
}
}
} }
val toolchains: Sequence<Pair<UUID, ZigToolchain>> val toolchains: Sequence<Pair<UUID, ZigToolchain>>
@ -72,4 +106,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
@JvmStatic @JvmStatic
fun getInstance(): ZigToolchainListService = service() fun getInstance(): ZigToolchainListService = service()
} }
@FunctionalInterface
interface ToolchainListChangeListener {
fun toolchainListChanged()
}
} }

View file

@ -22,16 +22,22 @@
package com.falsepattern.zigbrains.project.toolchain.downloader package com.falsepattern.zigbrains.project.toolchain.downloader
import com.falsepattern.zigbrains.shared.Unarchiver
import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.PathManager
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.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.util.progress.reportProgress
import com.intellij.platform.util.progress.reportSequentialProgress
import com.intellij.platform.util.progress.withProgressText 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.delete
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 kotlinx.coroutines.Dispatchers import com.intellij.util.text.SemVer
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
@ -40,46 +46,92 @@ import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive 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.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.isDirectory
@JvmRecord @JvmRecord
data class ZigVersionInfo(val date: String, val docs: String, val notes: String, val src: Tarball?, val dist: Tarball) { data class ZigVersionInfo(
companion object { val version: SemVer,
suspend fun download(): List<Pair<String, ZigVersionInfo>> { val date: String,
return withProgressText("Fetching zig version information") { val docs: String,
withContext(Dispatchers.IO) { val notes: String,
doDownload() val src: Tarball?,
} val dist: Tarball
) {
suspend fun downloadAndUnpack(into: Path): Boolean {
return reportProgress { reporter ->
try {
into.createDirectories()
} catch (e: Exception) {
return@reportProgress false
} }
}
@OptIn(ExperimentalSerializationApi::class)
private suspend fun doDownload(): List<Pair<String, ZigVersionInfo>> {
val service = DownloadableFileService.getInstance() val service = DownloadableFileService.getInstance()
val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json") val fileName = dist.tarball.substringAfterLast('/')
val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false)
val desc = service.createFileDescription(dist.tarball, tempFile.name)
val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") val downloader = service.createDownloader(listOf(desc), "Zig version information downloading")
val downloadDirectory = tempPluginDir.toFile() val downloadResults = reporter.sizedStep(100) {
val downloadResults = coroutineToIndicator { coroutineToIndicator {
downloader.download(downloadDirectory) downloader.download(into.toFile())
}
var info: JsonObject? = null
for (result in downloadResults) {
if (result.second.defaultFileName == "index.json") {
info = result.first.inputStream().use { Json.decodeFromStream<JsonObject>(it) }
} }
} }
return info?.mapNotNull { (version, data) -> parseVersion(data)?.let { Pair(version, it) } }?.toList() ?: emptyList() if (downloadResults.isEmpty())
return@reportProgress false
val tarball = downloadResults[0].first
reporter.indeterminateStep("Extracting tarball") {
Unarchiver.unarchive(tarball.toPath(), into)
tarball.delete()
val contents = Files.newDirectoryStream(into).use { it.toList() }
if (contents.size == 1 && contents[0].isDirectory()) {
val src = contents[0]
Files.newDirectoryStream(src).use { stream ->
stream.forEach {
it.move(into.resolve(src.relativize(it)))
}
}
src.delete()
}
}
return@reportProgress true
}
}
companion object {
@OptIn(ExperimentalSerializationApi::class)
suspend fun downloadVersionList(): List<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(data: JsonElement): ZigVersionInfo? { private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? {
data as? JsonObject ?: return null 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 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<Tarball>(it) }
val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } ?: return null val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) }
?: return null
return ZigVersionInfo(date, docs, notes, src, dist)
return ZigVersionInfo(version, date, docs, notes, src, dist)
} }
private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? {
@ -112,4 +164,4 @@ data class ZigVersionInfo(val date: String, val docs: String, val notes: String,
@Serializable @Serializable
data class Tarball(val tarball: String, val shasum: String, val size: Int) data class Tarball(val tarball: String, val shasum: String, val size: Int)
private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains") private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile()

View file

@ -24,64 +24,200 @@ package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.intellij.openapi.application.ModalityState import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT
import com.intellij.icons.AllIcons
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
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
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.components.textFieldWithBrowseButton
import com.intellij.ui.dsl.builder.AlignX 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.awt.Component import java.awt.Component
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID
import javax.swing.DefaultComboBoxModel import javax.swing.DefaultComboBoxModel
import javax.swing.JList
import javax.swing.event.DocumentEvent
import kotlin.contracts.ExperimentalContracts
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
//TODO lang
object Downloader { object Downloader {
suspend fun openDownloadDialog(component: Component) { suspend fun downloadToolchain(component: Component): UUID? {
val info = withModalProgress( val info = withModalProgress(
component.let { ModalTaskOwner.component(it) }, ModalTaskOwner.component(component),
"Fetching zig version information", "Fetching zig version information",
TaskCancellation.Companion.cancellable()) { TaskCancellation.cancellable()) {
ZigVersionInfo.Companion.download() withContext(Dispatchers.IO) {
ZigVersionInfo.downloadVersionList()
}
} }
withEDTContext(ModalityState.stateForComponent(component)) { val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) {
val dialog = DialogBuilder() selectToolchain(info)
val theList = ComboBox<String>(DefaultComboBoxModel(info.map { it.first }.toTypedArray())) } ?: return null
val outputPath = textFieldWithBrowseButton( withModalProgress(
null, ModalTaskOwner.component(component),
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) "Downloading zig tarball",
).also { TaskCancellation.cancellable()) {
Disposer.register(dialog, it) withContext(Dispatchers.IO) {
version.downloadAndUnpack(downloadPath)
} }
var archiveSizeCell: Cell<*>? = null }
fun detect(item: String) { return null
outputPath.text = System.getProperty("user.home") + "/.zig/" + item }
val data = info.firstOrNull { it.first == item } ?: return
val size = data.second.dist.size private enum class DirectoryState {
val sizeMb = size / (1024f * 1024f) Invalid,
archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) NotAbsolute,
NotDirectory,
NotEmpty,
CreateNew,
Ok;
fun isValid(): Boolean {
return when(this) {
Invalid, NotAbsolute, NotDirectory, NotEmpty -> false
CreateNew, Ok -> true
} }
theList.addItemListener { }
detect(it.item as String)
} companion object {
val center = panel { @OptIn(ExperimentalContracts::class)
row("Version:") { @JvmStatic
cell(theList).resizableColumn().align(AlignX.FILL) fun determine(path: Path?): DirectoryState {
if (path == null) {
return Invalid
} }
row("Location:") { if (!path.isAbsolute) {
cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } return NotAbsolute
} }
if (!path.exists()) {
var parent: Path? = path.parent
while(parent != null) {
if (!parent.exists()) {
parent = parent.parent
continue
}
if (!parent.isDirectory()) {
return NotDirectory
}
return CreateNew
}
return Invalid
}
if (!path.isDirectory()) {
return NotDirectory
}
val isEmpty = Files.newDirectoryStream(path).use { !it.iterator().hasNext() }
if (!isEmpty) {
return NotEmpty
}
return Ok
} }
detect(info[0].first)
dialog.centerPanel(center)
dialog.setTitle("Version Selector")
dialog.addCancelAction()
dialog.showAndGet()
Disposer.dispose(dialog)
} }
} }
private fun selectToolchain(info: List<ZigVersionInfo>): Pair<Path, ZigVersionInfo>? {
val dialog = DialogBuilder()
val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray()))
theList.renderer = object: ColoredListCellRenderer<ZigVersionInfo>() {
override fun customizeCellRenderer(
list: JList<out ZigVersionInfo>,
value: ZigVersionInfo?,
index: Int,
selected: Boolean,
hasFocus: Boolean
) {
value?.let { append(it.version.rawVersion) }
}
}
val outputPath = textFieldWithBrowseButton(
null,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
)
Disposer.register(dialog, outputPath)
outputPath.textField.columns = 50
lateinit var errorMessageBox: JBLabel
outputPath.addDocumentListener(object: DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
val path = outputPath.text.ifBlank { null }?.toNioPathOrNull()
val state = DirectoryState.determine(path)
if (state.isValid()) {
errorMessageBox.icon = AllIcons.General.Information
dialog.setOkActionEnabled(true)
} else {
errorMessageBox.icon = AllIcons.General.Error
dialog.setOkActionEnabled(false)
}
errorMessageBox.text = when(state) {
DirectoryState.Invalid -> "Invalid path"
DirectoryState.NotAbsolute -> "Must be an absolute path"
DirectoryState.NotDirectory -> "Path is not a directory"
DirectoryState.NotEmpty -> "Directory is not empty"
DirectoryState.CreateNew -> "Directory will be created"
DirectoryState.Ok -> "Directory OK"
}
dialog.window.repaint()
}
})
var archiveSizeCell: Cell<*>? = null
fun detect(item: ZigVersionInfo) {
outputPath.text = System.getProperty("user.home") + "/.zig/" + item.version
val size = item.dist.size
val sizeMb = size / (1024f * 1024f)
archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb)
}
theList.addItemListener {
detect(it.item as ZigVersionInfo)
}
val center = panel {
row("Version:") {
cell(theList).resizableColumn().align(AlignX.FILL)
}
row("Location:") {
cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") }
}
row {
errorMessageBox = JBLabel()
cell(errorMessageBox)
}
}
detect(info[0])
dialog.centerPanel(center)
dialog.setTitle("Version Selector")
dialog.addCancelAction()
dialog.addOkAction().also { it.setText("Download") }
if (!dialog.showAndGet()) {
return null
}
val path = outputPath.text.ifBlank { null }?.toNioPathOrNull()
?: return null
if (!DirectoryState.determine(path).isValid()) {
return null
}
val version = theList.selectedItem?.asSafely<ZigVersionInfo>()
?: return null
return path to version
}
private suspend fun installToolchain(path: Path, version: ZigVersionInfo): Boolean {
TODO("Not yet implemented")
}
} }

View file

@ -72,19 +72,15 @@ class ZigToolchainEditor(private val project: Project): Configurable {
super.disposeUIResources() super.disposeUIResources()
} }
inner class UI(): Disposable { inner class UI(): Disposable, ZigToolchainListService.ToolchainListChangeListener {
private val toolchainBox: TCComboBox private val toolchainBox: TCComboBox
private var oldSelectionIndex: Int = 0 private var oldSelectionIndex: Int = 0
private val model: TCModel
init { init {
val modelList = ArrayList<TCListElemIn>() model = TCModel(getModelList())
modelList.add(TCListElem.None) toolchainBox = TCComboBox(model)
modelList.addAll(ZigToolchainListService.Companion.getInstance().toolchains.map { it.asActual() })
modelList.add(Separator("", true))
modelList.addAll(TCListElem.fetchGroup)
modelList.add(Separator("Detected toolchains", true))
modelList.addAll(suggestZigToolchains().map { it.asSuggested() })
toolchainBox = TCComboBox(TCModel(modelList))
toolchainBox.addItemListener(::itemStateChanged) toolchainBox.addItemListener(::itemStateChanged)
ZigToolchainListService.getInstance().addChangeListener(this)
reset() reset()
} }
@ -107,6 +103,16 @@ class ZigToolchainEditor(private val project: Project): Configurable {
} }
} }
override fun toolchainListChanged() {
val selected = model.selected
val list = getModelList()
model.updateContents(list)
if (selected != null && list.contains(selected)) {
model.selectedItem = selected
} else {
model.selectedItem = TCListElem.None
}
}
fun attach(p: Panel): Unit = with(p) { fun attach(p: Panel): Unit = with(p) {
row("Toolchain") { row("Toolchain") {
@ -115,20 +121,32 @@ class ZigToolchainEditor(private val project: Project): Configurable {
} }
fun isModified(): Boolean { fun isModified(): Boolean {
return ZigToolchainService.Companion.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain return ZigToolchainService.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain
} }
fun apply() { fun apply() {
ZigToolchainService.Companion.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain ZigToolchainService.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain
} }
fun reset() { fun reset() {
toolchainBox.selectedToolchain = ZigToolchainService.Companion.getInstance(project).toolchainUUID toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID
oldSelectionIndex = toolchainBox.selectedIndex oldSelectionIndex = toolchainBox.selectedIndex
} }
override fun dispose() { override fun dispose() {
ZigToolchainListService.getInstance().removeChangeListener(this)
} }
} }
} }
private fun getModelList(): List<TCListElemIn> {
val modelList = ArrayList<TCListElemIn>()
modelList.add(TCListElem.None)
modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() })
modelList.add(Separator("", true))
modelList.addAll(TCListElem.fetchGroup)
modelList.add(Separator("Detected toolchains", true))
modelList.addAll(suggestZigToolchains().map { it.asSuggested() })
return modelList
}

View file

@ -27,6 +27,7 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.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.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
@ -34,23 +35,25 @@ import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.openapi.ui.MasterDetailsComponent
import com.intellij.util.IconUtil import com.intellij.util.IconUtil
import kotlinx.coroutines.async import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.tree.DefaultTreeModel import javax.swing.tree.DefaultTreeModel
class ZigToolchainListEditor() : MasterDetailsComponent() { class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener {
private var isTreeInitialized = false private var isTreeInitialized = false
private var myComponent: JComponent? = null private var registered: Boolean = false
override fun createComponent(): JComponent { override fun createComponent(): JComponent {
if (!isTreeInitialized) { if (!isTreeInitialized) {
initTree() initTree()
isTreeInitialized = true isTreeInitialized = true
} }
val comp = super.createComponent() if (!registered) {
myComponent = comp ZigToolchainListService.getInstance().addChangeListener(this)
return comp registered = true
}
return super.createComponent()
} }
override fun createActions(fromPopup: Boolean): List<AnAction> { override fun createActions(fromPopup: Boolean): List<AnAction> {
@ -81,12 +84,10 @@ class ZigToolchainListEditor() : MasterDetailsComponent() {
is TCListElem.Toolchain -> { is TCListElem.Toolchain -> {
val uuid = UUID.randomUUID() val uuid = UUID.randomUUID()
ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain) ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain)
addToolchain(uuid, elem.toolchain)
(myTree.model as DefaultTreeModel).reload()
} }
is TCListElem.Download -> { is TCListElem.Download -> {
zigCoroutineScope.async { zigCoroutineScope.launch(myWholePanel.asContextElement()) {
Downloader.openDownloadDialog(myComponent!!) Downloader.downloadToolchain(myWholePanel)
} }
} }
is TCListElem.FromDisk -> {} is TCListElem.FromDisk -> {}
@ -118,6 +119,12 @@ class ZigToolchainListEditor() : MasterDetailsComponent() {
override fun disposeUIResources() { override fun disposeUIResources() {
super.disposeUIResources() super.disposeUIResources()
myComponent = null if (registered) {
ZigToolchainListService.getInstance().removeChangeListener(this)
}
}
override fun toolchainListChanged() {
reloadTree()
} }
} }

View file

@ -85,9 +85,15 @@ internal class TCComboBox(model: TCModel): ComboBox<TCListElem>(model) {
} }
} }
internal class TCModel private constructor(elements: List<TCListElem>, private val separators: Map<TCListElem, Separator>) : CollectionComboBoxModel<TCListElem>(elements) { internal class TCModel private constructor(elements: List<TCListElem>, private var separators: Map<TCListElem, Separator>) : CollectionComboBoxModel<TCListElem>(elements) {
companion object { companion object {
operator fun invoke(input: List<TCListElemIn>): TCModel { operator fun invoke(input: List<TCListElemIn>): TCModel {
val (elements, separators) = convert(input)
val model = TCModel(elements, separators)
return model
}
private fun convert(input: List<TCListElemIn>): Pair<List<TCListElem>, Map<TCListElem, Separator>> {
val separators = IdentityHashMap<TCListElem, Separator>() val separators = IdentityHashMap<TCListElem, Separator>()
var lastSeparator: Separator? = null var lastSeparator: Separator? = null
val elements = ArrayList<TCListElem>() val elements = ArrayList<TCListElem>()
@ -104,12 +110,17 @@ internal class TCModel private constructor(elements: List<TCListElem>, private v
is Separator -> lastSeparator = it is Separator -> lastSeparator = it
} }
} }
val model = TCModel(elements, separators) return elements to separators
return model
} }
} }
fun separatorAbove(elem: TCListElem) = separators[elem] fun separatorAbove(elem: TCListElem) = separators[elem]
fun updateContents(input: List<TCListElemIn>) {
val (elements, separators) = convert(input)
this.separators = separators
replaceAll(elements)
}
} }
internal class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context<TCListElem> { internal class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context<TCListElem> {

View file

@ -0,0 +1,66 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.shared
import com.intellij.util.io.Decompressor
import kotlinx.coroutines.runInterruptible
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.name
enum class Unarchiver {
ZIP {
override val extension = "zip"
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
},
TAR_GZ {
override val extension = "tar.gz"
override fun createDecompressor(file: Path) = Decompressor.Tar(file)
},
TAR_XZ {
override val extension = "tar.xz"
override fun createDecompressor(file: Path) = Decompressor.Tar(file)
},
VSIX {
override val extension = "vsix"
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
};
protected abstract val extension: String
protected abstract fun createDecompressor(file: Path): Decompressor
companion object {
@Throws(IOException::class)
suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) {
runInterruptible {
val unarchiver = entries.find { archivePath.name.endsWith(it.extension) }
?: error("Unexpected archive type: $archivePath")
val dec = unarchiver.createDecompressor(archivePath)
if (prefix != null) {
dec.removePrefixPath(prefix)
}
dec.extract(dst)
}
}
}
}

View file

@ -30,6 +30,8 @@ import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.util.application import com.intellij.util.application
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.awt.Component
import kotlin.coroutines.CoroutineContext
inline fun <T> runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleFactory: () -> String, cancellationFactory: () -> TaskCancellation = {TaskCancellation.cancellable()}, noinline action: suspend CoroutineScope.() -> T): T { inline fun <T> runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleFactory: () -> String, cancellationFactory: () -> TaskCancellation = {TaskCancellation.cancellable()}, noinline action: suspend CoroutineScope.() -> T): T {
return if (application.isDispatchThread) { return if (application.isDispatchThread) {
@ -40,7 +42,11 @@ inline fun <T> runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleF
} }
suspend inline fun <T> withEDTContext(state: ModalityState, noinline block: suspend CoroutineScope.() -> T): T { suspend inline fun <T> withEDTContext(state: ModalityState, noinline block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.EDT + state.asContextElement(), block = block) return withEDTContext(state.asContextElement(), block = block)
}
suspend inline fun <T> withEDTContext(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.EDT + context, block = block)
} }
suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend CoroutineScope.() -> T): T { suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend CoroutineScope.() -> T): T {
@ -49,16 +55,17 @@ suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend Cor
} }
} }
fun <T, R> T.letBlocking(targetAction: suspend CoroutineScope.(T) -> R): R {
return runBlocking {
targetAction(this@letBlocking)
}
}
suspend inline fun <T> runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T { suspend inline fun <T> runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T {
return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction) return runInterruptibleEDT(state.asContextElement(), targetAction = targetAction)
}
suspend inline fun <T> runInterruptibleEDT(context: CoroutineContext, noinline targetAction: () -> T): T {
return runInterruptible(Dispatchers.EDT + context, block = targetAction)
} }
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 launch(Dispatchers.EDT + state.asContextElement(), block = block)
} }
fun Component.asContextElement(): CoroutineContext {
return ModalityState.stateForComponent(this).asContextElement()
}