Working change sync and downloader
This commit is contained in:
parent
9023026478
commit
9676b70821
9 changed files with 437 additions and 134 deletions
|
@ -23,6 +23,7 @@
|
|||
package com.falsepattern.zigbrains.debugger.toolchain
|
||||
|
||||
import com.falsepattern.zigbrains.debugger.ZigDebugBundle
|
||||
import com.falsepattern.zigbrains.shared.Unarchiver
|
||||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.openapi.application.PathManager
|
||||
|
@ -40,14 +41,12 @@ import com.intellij.ui.components.JBPanel
|
|||
import com.intellij.util.application
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.download.DownloadableFileService
|
||||
import com.intellij.util.io.Decompressor
|
||||
import com.intellij.util.system.CpuArch
|
||||
import com.intellij.util.system.OS
|
||||
import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager
|
||||
import com.jetbrains.cidr.execution.debugger.backend.bin.UrlProvider
|
||||
import com.jetbrains.cidr.execution.debugger.backend.lldb.LLDBDriverConfiguration
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
|
@ -329,38 +328,6 @@ class ZigDebuggerToolchainService {
|
|||
}
|
||||
}
|
||||
|
||||
private enum class Unarchiver {
|
||||
ZIP {
|
||||
override val extension = "zip"
|
||||
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
|
||||
},
|
||||
TAR {
|
||||
override val extension = "tar.gz"
|
||||
override fun createDecompressor(file: Path) = Decompressor.Tar(file)
|
||||
},
|
||||
VSIX {
|
||||
override val extension = "vsix"
|
||||
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
|
||||
};
|
||||
|
||||
protected abstract val extension: String
|
||||
protected abstract fun createDecompressor(file: Path): Decompressor
|
||||
|
||||
companion object {
|
||||
@Throws(IOException::class)
|
||||
suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) {
|
||||
runInterruptible {
|
||||
val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } ?: error("Unexpected archive type: $archivePath")
|
||||
val dec = unarchiver.createDecompressor(archivePath)
|
||||
if (prefix != null) {
|
||||
dec.removePrefixPath(prefix)
|
||||
}
|
||||
dec.extract(dst)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DownloadResult {
|
||||
class Ok(val baseDir: Path): DownloadResult()
|
||||
data object NoUrls: DownloadResult()
|
||||
|
|
|
@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
|||
import com.falsepattern.zigbrains.project.toolchain.base.resolve
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.toRef
|
||||
import com.intellij.openapi.components.*
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.UUID
|
||||
|
||||
@Service(Service.Level.APP)
|
||||
|
@ -34,13 +35,15 @@ import java.util.UUID
|
|||
storages = [Storage("zigbrains.xml")]
|
||||
)
|
||||
class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchainListService.State>(State()) {
|
||||
private val changeListeners = ArrayList<WeakReference<ToolchainListChangeListener>>()
|
||||
fun setToolchain(uuid: UUID, toolchain: ZigToolchain) {
|
||||
updateState {
|
||||
val newMap = HashMap<String, ZigToolchain.Ref>()
|
||||
newMap.putAll(it.toolchains)
|
||||
newMap.put(uuid.toString(), toolchain.toRef())
|
||||
newMap[uuid.toString()] = toolchain.toRef()
|
||||
it.copy(toolchains = newMap)
|
||||
}
|
||||
notifyChanged()
|
||||
}
|
||||
|
||||
fun getToolchain(uuid: UUID): ZigToolchain? {
|
||||
|
@ -52,6 +55,37 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
|
|||
updateState {
|
||||
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>>
|
||||
|
@ -72,4 +106,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
|
|||
@JvmStatic
|
||||
fun getInstance(): ZigToolchainListService = service()
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ToolchainListChangeListener {
|
||||
fun toolchainListChanged()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,16 +22,22 @@
|
|||
|
||||
package com.falsepattern.zigbrains.project.toolchain.downloader
|
||||
|
||||
import com.falsepattern.zigbrains.shared.Unarchiver
|
||||
import com.intellij.openapi.application.PathManager
|
||||
import com.intellij.openapi.progress.coroutineToIndicator
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
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.util.asSafely
|
||||
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.OS
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import com.intellij.util.text.SemVer
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -40,46 +46,92 @@ import kotlinx.serialization.json.JsonObject
|
|||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
@JvmRecord
|
||||
data class ZigVersionInfo(val date: String, val docs: String, val notes: String, val src: Tarball?, val dist: Tarball) {
|
||||
companion object {
|
||||
suspend fun download(): List<Pair<String, ZigVersionInfo>> {
|
||||
return withProgressText("Fetching zig version information") {
|
||||
withContext(Dispatchers.IO) {
|
||||
doDownload()
|
||||
}
|
||||
data class ZigVersionInfo(
|
||||
val version: SemVer,
|
||||
val date: String,
|
||||
val docs: String,
|
||||
val notes: String,
|
||||
val src: Tarball?,
|
||||
val dist: Tarball
|
||||
) {
|
||||
suspend fun downloadAndUnpack(into: Path): Boolean {
|
||||
return reportProgress { reporter ->
|
||||
try {
|
||||
into.createDirectories()
|
||||
} catch (e: Exception) {
|
||||
return@reportProgress false
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
private suspend fun doDownload(): List<Pair<String, ZigVersionInfo>> {
|
||||
val service = DownloadableFileService.getInstance()
|
||||
val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json")
|
||||
val fileName = dist.tarball.substringAfterLast('/')
|
||||
val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false)
|
||||
val desc = service.createFileDescription(dist.tarball, tempFile.name)
|
||||
val downloader = service.createDownloader(listOf(desc), "Zig version information downloading")
|
||||
val downloadDirectory = tempPluginDir.toFile()
|
||||
val downloadResults = coroutineToIndicator {
|
||||
downloader.download(downloadDirectory)
|
||||
}
|
||||
var info: JsonObject? = null
|
||||
for (result in downloadResults) {
|
||||
if (result.second.defaultFileName == "index.json") {
|
||||
info = result.first.inputStream().use { Json.decodeFromStream<JsonObject>(it) }
|
||||
val downloadResults = reporter.sizedStep(100) {
|
||||
coroutineToIndicator {
|
||||
downloader.download(into.toFile())
|
||||
}
|
||||
}
|
||||
return info?.mapNotNull { (version, data) -> parseVersion(data)?.let { Pair(version, it) } }?.toList() ?: emptyList()
|
||||
if (downloadResults.isEmpty())
|
||||
return@reportProgress false
|
||||
val tarball = downloadResults[0].first
|
||||
reporter.indeterminateStep("Extracting tarball") {
|
||||
Unarchiver.unarchive(tarball.toPath(), into)
|
||||
tarball.delete()
|
||||
val contents = Files.newDirectoryStream(into).use { it.toList() }
|
||||
if (contents.size == 1 && contents[0].isDirectory()) {
|
||||
val src = contents[0]
|
||||
Files.newDirectoryStream(src).use { stream ->
|
||||
stream.forEach {
|
||||
it.move(into.resolve(src.relativize(it)))
|
||||
}
|
||||
}
|
||||
src.delete()
|
||||
}
|
||||
}
|
||||
return@reportProgress true
|
||||
}
|
||||
}
|
||||
companion object {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun downloadVersionList(): List<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? {
|
||||
data as? JsonObject ?: return null
|
||||
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 notes = data["notes"]?.asSafely<JsonPrimitive>()?.content?: ""
|
||||
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? {
|
||||
|
@ -112,4 +164,4 @@ data class ZigVersionInfo(val date: String, val docs: String, val notes: String,
|
|||
@Serializable
|
||||
data class Tarball(val tarball: String, val shasum: String, val size: Int)
|
||||
|
||||
private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains")
|
||||
private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile()
|
|
@ -24,64 +24,200 @@ package com.falsepattern.zigbrains.project.toolchain.ui
|
|||
|
||||
import com.falsepattern.zigbrains.ZigBrainsBundle
|
||||
import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo
|
||||
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
|
||||
import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.DialogBuilder
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.platform.ide.progress.TaskCancellation
|
||||
import com.intellij.platform.ide.progress.withModalProgress
|
||||
import com.intellij.ui.ColoredListCellRenderer
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.textFieldWithBrowseButton
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.Cell
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.util.asSafely
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.awt.Component
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.UUID
|
||||
import javax.swing.DefaultComboBoxModel
|
||||
import javax.swing.JList
|
||||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
|
||||
//TODO lang
|
||||
object Downloader {
|
||||
suspend fun openDownloadDialog(component: Component) {
|
||||
suspend fun downloadToolchain(component: Component): UUID? {
|
||||
val info = withModalProgress(
|
||||
component.let { ModalTaskOwner.component(it) },
|
||||
ModalTaskOwner.component(component),
|
||||
"Fetching zig version information",
|
||||
TaskCancellation.Companion.cancellable()) {
|
||||
ZigVersionInfo.Companion.download()
|
||||
TaskCancellation.cancellable()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
ZigVersionInfo.downloadVersionList()
|
||||
}
|
||||
}
|
||||
withEDTContext(ModalityState.stateForComponent(component)) {
|
||||
val dialog = DialogBuilder()
|
||||
val theList = ComboBox<String>(DefaultComboBoxModel(info.map { it.first }.toTypedArray()))
|
||||
val outputPath = textFieldWithBrowseButton(
|
||||
null,
|
||||
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
|
||||
).also {
|
||||
Disposer.register(dialog, it)
|
||||
val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) {
|
||||
selectToolchain(info)
|
||||
} ?: return null
|
||||
withModalProgress(
|
||||
ModalTaskOwner.component(component),
|
||||
"Downloading zig tarball",
|
||||
TaskCancellation.cancellable()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
version.downloadAndUnpack(downloadPath)
|
||||
}
|
||||
var archiveSizeCell: Cell<*>? = null
|
||||
fun detect(item: String) {
|
||||
outputPath.text = System.getProperty("user.home") + "/.zig/" + item
|
||||
val data = info.firstOrNull { it.first == item } ?: return
|
||||
val size = data.second.dist.size
|
||||
val sizeMb = size / (1024f * 1024f)
|
||||
archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private enum class DirectoryState {
|
||||
Invalid,
|
||||
NotAbsolute,
|
||||
NotDirectory,
|
||||
NotEmpty,
|
||||
CreateNew,
|
||||
Ok;
|
||||
|
||||
fun isValid(): Boolean {
|
||||
return when(this) {
|
||||
Invalid, NotAbsolute, NotDirectory, NotEmpty -> false
|
||||
CreateNew, Ok -> true
|
||||
}
|
||||
theList.addItemListener {
|
||||
detect(it.item as String)
|
||||
}
|
||||
val center = panel {
|
||||
row("Version:") {
|
||||
cell(theList).resizableColumn().align(AlignX.FILL)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
@JvmStatic
|
||||
fun determine(path: Path?): DirectoryState {
|
||||
if (path == null) {
|
||||
return Invalid
|
||||
}
|
||||
row("Location:") {
|
||||
cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") }
|
||||
if (!path.isAbsolute) {
|
||||
return NotAbsolute
|
||||
}
|
||||
if (!path.exists()) {
|
||||
var parent: Path? = path.parent
|
||||
while(parent != null) {
|
||||
if (!parent.exists()) {
|
||||
parent = parent.parent
|
||||
continue
|
||||
}
|
||||
if (!parent.isDirectory()) {
|
||||
return NotDirectory
|
||||
}
|
||||
return CreateNew
|
||||
}
|
||||
return Invalid
|
||||
}
|
||||
if (!path.isDirectory()) {
|
||||
return NotDirectory
|
||||
}
|
||||
val isEmpty = Files.newDirectoryStream(path).use { !it.iterator().hasNext() }
|
||||
if (!isEmpty) {
|
||||
return NotEmpty
|
||||
}
|
||||
return Ok
|
||||
}
|
||||
detect(info[0].first)
|
||||
dialog.centerPanel(center)
|
||||
dialog.setTitle("Version Selector")
|
||||
dialog.addCancelAction()
|
||||
dialog.showAndGet()
|
||||
Disposer.dispose(dialog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectToolchain(info: List<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")
|
||||
}
|
||||
}
|
|
@ -72,19 +72,15 @@ class ZigToolchainEditor(private val project: Project): Configurable {
|
|||
super.disposeUIResources()
|
||||
}
|
||||
|
||||
inner class UI(): Disposable {
|
||||
inner class UI(): Disposable, ZigToolchainListService.ToolchainListChangeListener {
|
||||
private val toolchainBox: TCComboBox
|
||||
private var oldSelectionIndex: Int = 0
|
||||
private val model: TCModel
|
||||
init {
|
||||
val modelList = ArrayList<TCListElemIn>()
|
||||
modelList.add(TCListElem.None)
|
||||
modelList.addAll(ZigToolchainListService.Companion.getInstance().toolchains.map { it.asActual() })
|
||||
modelList.add(Separator("", true))
|
||||
modelList.addAll(TCListElem.fetchGroup)
|
||||
modelList.add(Separator("Detected toolchains", true))
|
||||
modelList.addAll(suggestZigToolchains().map { it.asSuggested() })
|
||||
toolchainBox = TCComboBox(TCModel(modelList))
|
||||
model = TCModel(getModelList())
|
||||
toolchainBox = TCComboBox(model)
|
||||
toolchainBox.addItemListener(::itemStateChanged)
|
||||
ZigToolchainListService.getInstance().addChangeListener(this)
|
||||
reset()
|
||||
}
|
||||
|
||||
|
@ -107,6 +103,16 @@ class ZigToolchainEditor(private val project: Project): Configurable {
|
|||
}
|
||||
}
|
||||
|
||||
override fun toolchainListChanged() {
|
||||
val selected = model.selected
|
||||
val list = getModelList()
|
||||
model.updateContents(list)
|
||||
if (selected != null && list.contains(selected)) {
|
||||
model.selectedItem = selected
|
||||
} else {
|
||||
model.selectedItem = TCListElem.None
|
||||
}
|
||||
}
|
||||
|
||||
fun attach(p: Panel): Unit = with(p) {
|
||||
row("Toolchain") {
|
||||
|
@ -115,20 +121,32 @@ class ZigToolchainEditor(private val project: Project): Configurable {
|
|||
}
|
||||
|
||||
fun isModified(): Boolean {
|
||||
return ZigToolchainService.Companion.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain
|
||||
return ZigToolchainService.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain
|
||||
}
|
||||
|
||||
fun apply() {
|
||||
ZigToolchainService.Companion.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain
|
||||
ZigToolchainService.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
toolchainBox.selectedToolchain = ZigToolchainService.Companion.getInstance(project).toolchainUUID
|
||||
toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID
|
||||
oldSelectionIndex = toolchainBox.selectedIndex
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
|
||||
ZigToolchainListService.getInstance().removeChangeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getModelList(): List<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
|
||||
}
|
|
@ -27,6 +27,7 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
|
|||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
|
||||
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
|
||||
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
||||
import com.intellij.openapi.actionSystem.AnAction
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
|
@ -34,23 +35,25 @@ import com.intellij.openapi.actionSystem.Presentation
|
|||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.ui.MasterDetailsComponent
|
||||
import com.intellij.util.IconUtil
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.tree.DefaultTreeModel
|
||||
|
||||
class ZigToolchainListEditor() : MasterDetailsComponent() {
|
||||
class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener {
|
||||
private var isTreeInitialized = false
|
||||
private var myComponent: JComponent? = null
|
||||
private var registered: Boolean = false
|
||||
|
||||
override fun createComponent(): JComponent {
|
||||
if (!isTreeInitialized) {
|
||||
initTree()
|
||||
isTreeInitialized = true
|
||||
}
|
||||
val comp = super.createComponent()
|
||||
myComponent = comp
|
||||
return comp
|
||||
if (!registered) {
|
||||
ZigToolchainListService.getInstance().addChangeListener(this)
|
||||
registered = true
|
||||
}
|
||||
return super.createComponent()
|
||||
}
|
||||
|
||||
override fun createActions(fromPopup: Boolean): List<AnAction> {
|
||||
|
@ -81,12 +84,10 @@ class ZigToolchainListEditor() : MasterDetailsComponent() {
|
|||
is TCListElem.Toolchain -> {
|
||||
val uuid = UUID.randomUUID()
|
||||
ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain)
|
||||
addToolchain(uuid, elem.toolchain)
|
||||
(myTree.model as DefaultTreeModel).reload()
|
||||
}
|
||||
is TCListElem.Download -> {
|
||||
zigCoroutineScope.async {
|
||||
Downloader.openDownloadDialog(myComponent!!)
|
||||
zigCoroutineScope.launch(myWholePanel.asContextElement()) {
|
||||
Downloader.downloadToolchain(myWholePanel)
|
||||
}
|
||||
}
|
||||
is TCListElem.FromDisk -> {}
|
||||
|
@ -118,6 +119,12 @@ class ZigToolchainListEditor() : MasterDetailsComponent() {
|
|||
|
||||
override fun disposeUIResources() {
|
||||
super.disposeUIResources()
|
||||
myComponent = null
|
||||
if (registered) {
|
||||
ZigToolchainListService.getInstance().removeChangeListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun toolchainListChanged() {
|
||||
reloadTree()
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
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>()
|
||||
var lastSeparator: Separator? = null
|
||||
val elements = ArrayList<TCListElem>()
|
||||
|
@ -104,12 +110,17 @@ internal class TCModel private constructor(elements: List<TCListElem>, private v
|
|||
is Separator -> lastSeparator = it
|
||||
}
|
||||
}
|
||||
val model = TCModel(elements, separators)
|
||||
return model
|
||||
return elements to separators
|
||||
}
|
||||
}
|
||||
|
||||
fun separatorAbove(elem: TCListElem) = separators[elem]
|
||||
|
||||
fun updateContents(input: List<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> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,6 +30,8 @@ import com.intellij.platform.ide.progress.TaskCancellation
|
|||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.util.application
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.Component
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
inline fun <T> runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleFactory: () -> String, cancellationFactory: () -> TaskCancellation = {TaskCancellation.cancellable()}, noinline action: suspend CoroutineScope.() -> T): T {
|
||||
return if (application.isDispatchThread) {
|
||||
|
@ -40,7 +42,11 @@ inline fun <T> runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleF
|
|||
}
|
||||
|
||||
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 {
|
||||
|
@ -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 {
|
||||
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 {
|
||||
return launch(Dispatchers.EDT + state.asContextElement(), block = block)
|
||||
}
|
||||
|
||||
fun Component.asContextElement(): CoroutineContext {
|
||||
return ModalityState.stateForComponent(this).asContextElement()
|
||||
}
|
Loading…
Add table
Reference in a new issue