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
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()

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.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()
}
}

View file

@ -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()

View file

@ -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")
}
}

View file

@ -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
}

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.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()
}
}

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 {
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> {

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.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()
}