From 8bb4e8bef10a0dd755113c9f332246e90537fef8 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Sun, 6 Apr 2025 17:19:26 +0200 Subject: [PATCH] project-level toolchain selector --- .../toolchain/ZigToolchainListEditor.kt | 332 ------------------ .../toolchain/ZigToolchainListService.kt | 7 +- .../project/toolchain/ZigToolchainService.kt | 63 ++++ .../project/toolchain/ZigVersionInfo.kt | 106 ------ .../project/toolchain/base/ZigToolchain.kt | 5 +- .../base/ZigToolchainConfigurable.kt | 86 +++++ .../toolchain/base/ZigToolchainPanel.kt | 47 +++ .../toolchain/base/ZigToolchainProvider.kt | 10 +- .../toolchain/downloader/ZigVersionInfo.kt | 115 ++++++ .../toolchain/local/LocalZigToolchain.kt | 2 +- .../local/LocalZigToolchainConfigurable.kt | 71 +--- .../toolchain/local/LocalZigToolchainPanel.kt | 35 +- .../local/LocalZigToolchainProvider.kt | 8 +- .../project/toolchain/ui/Downloader.kt | 87 +++++ .../toolchain/ui/ZigToolchainEditor.kt | 134 +++++++ .../toolchain/ui/ZigToolchainListEditor.kt | 123 +++++++ .../project/toolchain/ui/elements.kt | 55 +++ .../zigbrains/project/toolchain/ui/model.kt | 215 ++++++++++++ .../project/toolchain/ui/popup/popup.kt | 49 +++ .../resources/META-INF/zigbrains-core.xml | 4 +- 20 files changed, 1016 insertions(+), 538 deletions(-) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt deleted file mode 100644 index fb917897..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt +++ /dev/null @@ -1,332 +0,0 @@ -/* - * 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 . - */ - -package com.falsepattern.zigbrains.project.toolchain - -import com.falsepattern.zigbrains.Icons -import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable -import com.falsepattern.zigbrains.project.toolchain.base.render -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.icons.AllIcons -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.Presentation -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.ui.DialogBuilder -import com.intellij.openapi.ui.MasterDetailsComponent -import com.intellij.openapi.util.Disposer -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.* -import com.intellij.ui.components.panels.OpaquePanel -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.ui.popup.list.ComboBoxPopup -import com.intellij.util.Consumer -import com.intellij.util.IconUtil -import com.intellij.util.ui.EmptyIcon -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import kotlinx.coroutines.async -import java.awt.BorderLayout -import java.awt.Component -import java.util.* -import javax.accessibility.AccessibleContext -import javax.swing.DefaultComboBoxModel -import javax.swing.JComponent -import javax.swing.JList -import javax.swing.ListCellRenderer -import javax.swing.border.Border -import javax.swing.tree.DefaultTreeModel - -class ZigToolchainListEditor() : MasterDetailsComponent() { - private var isTreeInitialized = false - private var myComponent: JComponent? = null - - override fun createComponent(): JComponent { - if (!isTreeInitialized) { - initTree() - isTreeInitialized = true - } - val comp = super.createComponent() - myComponent = comp - return comp - } - - override fun createActions(fromPopup: Boolean): List { - val add = object : DumbAwareAction({ "lmaoo" }, Presentation.NULL_STRING, IconUtil.addIcon) { - override fun actionPerformed(e: AnActionEvent) { - val toolchains = suggestZigToolchains(zigToolchainList.toolchains.map { it.second }.toList()) - val final = ArrayList() - final.add(TCListElem.Download) - final.add(TCListElem.FromDisk) - final.add(Separator("Detected toolchains", true)) - final.addAll(toolchains.map { TCListElem.Toolchain(it) }) - val model = TCModel(final) - val context = TCContext(null, model) - val popup = TCPopup(context, null, ::onItemSelected) - popup.showInBestPositionFor(e.dataContext) - } - } - return listOf(add, MyDeleteAction()) - } - - override fun onItemDeleted(item: Any?) { - if (item is UUID) { - zigToolchainList.removeToolchain(item) - } - super.onItemDeleted(item) - } - - private fun onItemSelected(elem: TCListElem) { - when (elem) { - is TCListElem.Toolchain -> { - val uuid = UUID.randomUUID() - zigToolchainList.setToolchain(uuid, elem.toolchain) - addToolchain(uuid, elem.toolchain) - (myTree.model as DefaultTreeModel).reload() - } - - is TCListElem.Download -> { - zigCoroutineScope.async { - withEDTContext(ModalityState.stateForComponent(myComponent!!)) { - val info = withModalProgress(myComponent?.let { ModalTaskOwner.component(it) } ?: ModalTaskOwner.guess(), "Fetching zig version information", TaskCancellation.cancellable()) { - ZigVersionInfo.download() - } - val dialog = DialogBuilder() - val theList = ComboBox(DefaultComboBoxModel(info.map { it.first }.toTypedArray())) - val outputPath = textFieldWithBrowseButton( - null, - FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) - ).also { - Disposer.register(dialog, it) - } - 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) - } - theList.addItemListener { - detect(it.item as String) - } - val center = panel { - row("Version:") { - cell(theList).resizableColumn().align(AlignX.FILL) - } - row("Location:") { - cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } - } - } - detect(info[0].first) - dialog.centerPanel(center) - dialog.setTitle("Version Selector") - dialog.addCancelAction() - dialog.showAndGet() - } - } - } - - is TCListElem.FromDisk -> {} - } - } - - override fun reset() { - reloadTree() - super.reset() - } - - override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchains.empty") - - override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") - - private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { - val node = MyNode(toolchain.createNamedConfigurable(uuid, ProjectManager.getInstance().defaultProject)) - addNode(node, myRoot) - } - - private fun reloadTree() { - myRoot.removeAllChildren() - zigToolchainList.toolchains.forEach { (uuid, toolchain) -> - addToolchain(uuid, toolchain) - } - (myTree.model as DefaultTreeModel).reload() - } - - override fun disposeUIResources() { - super.disposeUIResources() - myComponent = null - } -} - -private sealed interface TCListElemIn - -private sealed interface TCListElem : TCListElemIn { - @JvmRecord - data class Toolchain(val toolchain: ZigToolchain) : TCListElem - object Download : TCListElem - object FromDisk : TCListElem -} - -@JvmRecord -private data class Separator(val text: String, val separatorBar: Boolean) : TCListElemIn - -private class TCPopup( - context: TCContext, - selected: TCListElem?, - onItemSelected: Consumer, -) : ComboBoxPopup(context, selected, onItemSelected) - -private class TCModel private constructor(elements: List, private val separators: Map) : CollectionListModel(elements) { - companion object { - operator fun invoke(input: List): TCModel { - val separators = IdentityHashMap() - var lastSeparator: Separator? = null - val elements = ArrayList() - input.forEach { - when (it) { - is TCListElem -> { - if (lastSeparator != null) { - separators[it] = lastSeparator - lastSeparator = null - } - elements.add(it) - } - - is Separator -> lastSeparator = it - } - } - val model = TCModel(elements, separators) - return model - } - } - - fun separatorAbove(elem: TCListElem) = separators[elem] -} - -private class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { - override fun getProject(): Project? { - return project - } - - override fun getModel(): TCModel { - return model - } - - override fun getRenderer(): ListCellRenderer { - return TCCellRenderer(::getModel) - } -} - -private class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer() { - - override fun getListCellRendererComponent( - list: JList?, - value: TCListElem?, - index: Int, - selected: Boolean, - hasFocus: Boolean - ): Component? { - val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent - val panel = object : CellRendererPanel(BorderLayout()) { - val myContext = component.accessibleContext - - override fun getAccessibleContext(): AccessibleContext? { - return myContext - } - - override fun setBorder(border: Border?) { - component.border = border - } - } - panel.add(component, BorderLayout.CENTER) - - component.isOpaque = true - list?.let { background = if (selected) it.selectionBackground else it.background } - - val model = getModel() - - val separator = value?.let { model.separatorAbove(it) } - - if (separator != null) { - val separatorText = separator.text - val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 - val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) - separatorComponent.isHideLine = !separator.separatorBar - if (separatorText.isNotBlank()) { - separatorComponent.caption = separatorText - } - - val wrapper = OpaquePanel(BorderLayout()) - wrapper.add(separatorComponent, BorderLayout.CENTER) - list?.let { wrapper.background = it.background } - panel.add(wrapper, BorderLayout.NORTH) - } - - return panel - } - - override fun customizeCellRenderer( - list: JList, - value: TCListElem?, - index: Int, - selected: Boolean, - hasFocus: Boolean - ) { - icon = EMPTY_ICON - when (value) { - is TCListElem.Toolchain -> { - icon = Icons.Zig - val toolchain = value.toolchain - toolchain.render(this) - } - - is TCListElem.Download -> { - icon = AllIcons.Actions.Download - append("Download Zig\u2026") - } - - is TCListElem.FromDisk -> { - icon = AllIcons.General.OpenDisk - append("Add Zig from disk\u2026") - } - - null -> {} - } - } -} - -private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index b53a4a0e..d12c54ce 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -67,6 +67,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent = emptyMap(), ) -} -val zigToolchainList get() = service() \ No newline at end of file + companion object { + @JvmStatic + fun getInstance(): ZigToolchainListService = service() + } +} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt new file mode 100644 index 00000000..e08ea5d6 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -0,0 +1,63 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.intellij.openapi.components.SerializablePersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute +import java.util.UUID + +@Service(Service.Level.PROJECT) +@State( + name = "ZigToolchain", + storages = [Storage("zigbrains.xml")] +) +class ZigToolchainService: SerializablePersistentStateComponent(State()) { + var toolchainUUID: UUID? + get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) } + set(value) { + updateState { + it.copy(toolchain = value?.toString() ?: "") + } + } + + val toolchain: ZigToolchain? + get() = toolchainUUID?.let { ZigToolchainListService.getInstance().getToolchain(it) } + + @JvmRecord + data class State( + @JvmField + @Attribute + val toolchain: String = "" + ) + + companion object { + @JvmStatic + fun getInstance(project: Project): ZigToolchainService = project.service() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt deleted file mode 100644 index dcfc7758..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 . - */ - -package com.falsepattern.zigbrains.project.toolchain - -import com.intellij.openapi.application.PathManager -import com.intellij.openapi.progress.coroutineToIndicator -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.platform.util.progress.withProgressText -import com.intellij.util.asSafely -import com.intellij.util.download.DownloadableFileService -import com.intellij.util.system.CpuArch -import com.intellij.util.system.OS -import com.intellij.util.text.SemVer -import com.jetbrains.rd.util.firstOrNull -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.decodeFromStream -import java.nio.file.Path - -@JvmRecord -data class ZigVersionInfo(val date: String, val docs: String, val notes: String, val src: Tarball?, val dist: Tarball) { - companion object { - @OptIn(ExperimentalSerializationApi::class) - suspend fun download(): List> { - return withProgressText("Fetching zig version information") { - withContext(Dispatchers.IO) { - val service = DownloadableFileService.getInstance() - val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json") - 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(it) } - } - } - info?.mapNotNull getVersions@{ (version, data) -> - data as? JsonObject ?: return@getVersions null - val date = data["date"]?.asSafely()?.content ?: "" - val docs = data["docs"]?.asSafely()?.content ?: "" - val notes = data["notes"]?.asSafely()?.content ?: "" - val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } - val dist = data.firstNotNullOfOrNull findCompatible@{ (dist, tb) -> - if (!dist.contains('-')) - return@findCompatible null - val (arch, os) = dist.split('-', limit = 2) - val theArch = when(arch) { - "x86_64" -> CpuArch.X86_64 - "i386" -> CpuArch.X86 - "armv7a" -> CpuArch.ARM32 - "aarch64" -> CpuArch.ARM64 - else -> return@findCompatible null - } - val theOS = when(os) { - "linux" -> OS.Linux - "windows" -> OS.Windows - "macos" -> OS.macOS - "freebsd" -> OS.FreeBSD - else -> return@findCompatible null - } - if (theArch == CpuArch.CURRENT && theOS == OS.CURRENT) { - Json.decodeFromJsonElement(tb) - } else null - } ?: return@getVersions null - Pair(version, ZigVersionInfo(date, docs, notes, src, dist)) - } ?.toList() ?: emptyList() - } - } - } - } -} - -@JvmRecord -@Serializable -data class Tarball(val tarball: String, val shasum: String, val size: Int) - -private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains") \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt index 1d381947..e3cdfc2d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -25,13 +25,16 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project +import com.intellij.openapi.util.UserDataHolderBase import com.intellij.util.xmlb.annotations.Attribute import com.intellij.util.xmlb.annotations.MapAnnotation import java.nio.file.Path -abstract class ZigToolchain { +abstract class ZigToolchain: UserDataHolderBase() { val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) } + abstract val name: String? + abstract fun workingDirectory(project: Project? = null): Path? abstract suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt new file mode 100644 index 00000000..a125fe72 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -0,0 +1,86 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.dsl.builder.panel +import java.util.UUID +import javax.swing.JComponent + +abstract class ZigToolchainConfigurable( + val uuid: UUID, + tc: T +): NamedConfigurable() { + var toolchain: T = tc + set(value) { + ZigToolchainListService.getInstance().setToolchain(uuid, value) + field = value + } + private var myView: ZigToolchainPanel? = null + + abstract fun createPanel(): ZigToolchainPanel + + override fun createOptionsPanel(): JComponent? { + var view = myView + if (view == null) { + view = createPanel() + view.reset(toolchain) + myView = view + } + return panel { + view.attach(this) + } + } + + override fun getEditableObject(): UUID? { + return uuid + } + + override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { + return displayName + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return toolchain.name + } + + override fun isModified(): Boolean { + return myView?.isModified(toolchain) == true + } + + override fun apply() { + myView?.apply(toolchain)?.let { toolchain = it } + } + + override fun reset() { + myView?.reset(toolchain) + } + + override fun disposeUIResources() { + myView?.dispose() + myView = null + super.disposeUIResources() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt new file mode 100644 index 00000000..ab747375 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt @@ -0,0 +1,47 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.intellij.openapi.Disposable +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel + +abstract class ZigToolchainPanel: Disposable { + private val nameField = JBTextField() + + protected var nameFieldValue: String? + get() = nameField.text.ifBlank { null } + set(value) {nameField.text = value ?: ""} + + open fun attach(p: Panel): Unit = with(p) { + row("Name") { + cell(nameField).resizableColumn().align(AlignX.FILL) + } + separator() + } + + abstract fun isModified(toolchain: T): Boolean + abstract fun apply(toolchain: T): T? + abstract fun reset(toolchain: T) +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt index 9cf1dee4..ec1362bd 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable @@ -39,7 +40,7 @@ internal interface ZigToolchainProvider { fun deserialize(data: Map): ZigToolchain? fun serialize(toolchain: ZigToolchain): Map fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean - fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, project: Project): NamedConfigurable + fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> fun suggestToolchains(): List fun render(toolchain: ZigToolchain, component: SimpleColoredComponent) } @@ -60,12 +61,13 @@ suspend fun Project?.suggestZigToolchain(extraData: UserDataHolder): ZigToolchai return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(this, extraData) } } -fun ZigToolchain.createNamedConfigurable(uuid: UUID, project: Project): NamedConfigurable { +fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*> { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return provider.createConfigurable(uuid, this, project) + return provider.createConfigurable(uuid, this) } -fun suggestZigToolchains(existing: List): List { +fun suggestZigToolchains(): List { + val existing = ZigToolchainListService.getInstance().toolchains.map { (uuid, tc) -> tc }.toList() return EXTENSION_POINT_NAME.extensionList.flatMap { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } val suggestions = ext.suggestToolchains() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt new file mode 100644 index 00000000..0a2c4311 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt @@ -0,0 +1,115 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain.downloader + +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.progress.coroutineToIndicator +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.platform.util.progress.withProgressText +import com.intellij.util.asSafely +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.system.CpuArch +import com.intellij.util.system.OS +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.decodeFromStream +import java.nio.file.Path + +@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> { + return withProgressText("Fetching zig version information") { + withContext(Dispatchers.IO) { + doDownload() + } + } + } + + @OptIn(ExperimentalSerializationApi::class) + private suspend fun doDownload(): List> { + val service = DownloadableFileService.getInstance() + val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json") + 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(it) } + } + } + return info?.mapNotNull { (version, data) -> parseVersion(data)?.let { Pair(version, it) } }?.toList() ?: emptyList() + } + + private fun parseVersion(data: JsonElement): ZigVersionInfo? { + data as? JsonObject ?: return null + val date = data["date"]?.asSafely()?.content ?: "" + val docs = data["docs"]?.asSafely()?.content ?: "" + val notes = data["notes"]?.asSafely()?.content ?: "" + val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } + val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } ?: return null + + return ZigVersionInfo(date, docs, notes, src, dist) + } + + private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { + if (!dist.contains('-')) + return null + val (arch, os) = dist.split('-', limit = 2) + val theArch = when (arch) { + "x86_64" -> CpuArch.X86_64 + "i386" -> CpuArch.X86 + "armv7a" -> CpuArch.ARM32 + "aarch64" -> CpuArch.ARM64 + else -> return null + } + val theOS = when (os) { + "linux" -> OS.Linux + "windows" -> OS.Windows + "macos" -> OS.macOS + "freebsd" -> OS.FreeBSD + else -> return null + } + if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) { + return null + } + return Json.decodeFromJsonElement(tb) + } + } +} + +@JvmRecord +@Serializable +data class Tarball(val tarball: String, val shasum: String, val size: Int) + +private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains") \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index cbb4d47f..f23cb30b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -35,7 +35,7 @@ import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull import java.nio.file.Path -data class LocalZigToolchain(val location: Path, val std: Path? = null, val name: String? = null): ZigToolchain() { +data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain() { override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt index 2ceb6618..bc8e5adc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt @@ -22,75 +22,16 @@ package com.falsepattern.zigbrains.project.toolchain.local -import com.falsepattern.zigbrains.project.toolchain.zigToolchainList -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.NamedConfigurable -import com.intellij.openapi.util.NlsContexts -import com.intellij.ui.dsl.builder.panel -import kotlinx.coroutines.runBlocking +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import java.util.UUID -import javax.swing.JComponent class LocalZigToolchainConfigurable( - val uuid: UUID, - toolchain: LocalZigToolchain, - private val project: Project -): NamedConfigurable() { - var toolchain: LocalZigToolchain = toolchain - set(value) { - zigToolchainList.setToolchain(uuid, value) - field = value - } - private var myView: LocalZigToolchainPanel? = null + uuid: UUID, + toolchain: LocalZigToolchain +): ZigToolchainConfigurable(uuid, toolchain) { + override fun createPanel() = LocalZigToolchainPanel() + override fun setDisplayName(name: String?) { toolchain = toolchain.copy(name = name) } - - override fun getEditableObject(): UUID { - return uuid - } - - override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { - return displayName - } - - override fun createOptionsPanel(): JComponent? { - var view = myView - if (view == null) { - view = LocalZigToolchainPanel() - view.reset(this) - myView = view - } - return panel { - view.attach(this) - } - } - - override fun getDisplayName(): @NlsContexts.ConfigurableName String? { - var theName = toolchain.name - if (theName == null) { - val version = toolchain.zig.let { runBlocking { it.getEnv(project) } }.getOrNull()?.version - if (version != null) { - theName = "Zig $version" - toolchain = toolchain.copy(name = theName) - } - } - return theName - } - - override fun isModified(): Boolean { - return myView?.isModified(this) == true - } - - override fun apply() { - myView?.apply(this) - } - - override fun reset() { - myView?.reset(this) - } - - override fun disposeUIResources() { - super.disposeUIResources() - } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt index d486a7f3..d2301d35 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt @@ -23,6 +23,7 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainPanel import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.Disposable @@ -45,8 +46,7 @@ import kotlinx.coroutines.launch import javax.swing.event.DocumentEvent import kotlin.io.path.pathString -class LocalZigToolchainPanel() : Disposable { - private val nameField = JBTextField() +class LocalZigToolchainPanel() : ZigToolchainPanel() { private val pathToToolchain = textFieldWithBrowseButton( null, FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) @@ -75,11 +75,8 @@ class LocalZigToolchainPanel() : Disposable { ).also { Disposer.register(this, it) } private var debounce: Job? = null - fun attach(p: Panel): Unit = with(p) { - row("Name") { - cell(nameField).resizableColumn().align(AlignX.FILL) - } - separator() + override fun attach(p: Panel): Unit = with(p) { + super.attach(p) row(ZigBrainsBundle.message("settings.project.label.toolchain")) { cell(pathToToolchain).resizableColumn().align(AlignX.FILL) } @@ -92,27 +89,23 @@ class LocalZigToolchainPanel() : Disposable { } } - fun isModified(cfg: LocalZigToolchainConfigurable): Boolean { - val name = nameField.text.ifBlank { null } ?: return false - val tc = cfg.toolchain + override fun isModified(toolchain: LocalZigToolchain): Boolean { + val name = nameFieldValue ?: return false val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null - return name != cfg.displayName || tc.location != location || tc.std != std + return name != toolchain.name || toolchain.location != location || toolchain.std != std } - fun apply(cfg: LocalZigToolchainConfigurable): Boolean { - val tc = cfg.toolchain - val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false + override fun apply(toolchain: LocalZigToolchain): LocalZigToolchain? { + val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return null val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null - cfg.toolchain = tc.copy(location = location, std = std, name = nameField.text ?: "") - return true + return toolchain.copy(location = location, std = std, name = nameFieldValue ?: "") } - fun reset(cfg: LocalZigToolchainConfigurable) { - nameField.text = cfg.displayName ?: "" - val tc = cfg.toolchain - this.pathToToolchain.text = tc.location.pathString - val std = tc.std + override fun reset(toolchain: LocalZigToolchain) { + nameFieldValue = toolchain.name + this.pathToToolchain.text = toolchain.location.pathString + val std = toolchain.std if (std != null) { stdFieldOverride.isSelected = true pathToStd.text = std.pathString diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index 5d63ee66..e4761617 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.direnv.emptyEnv import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable @@ -85,11 +86,10 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override fun createConfigurable( uuid: UUID, - toolchain: ZigToolchain, - project: Project - ): NamedConfigurable { + toolchain: ZigToolchain + ): ZigToolchainConfigurable<*> { toolchain as LocalZigToolchain - return LocalZigToolchainConfigurable(uuid, toolchain, project) + return LocalZigToolchainConfigurable(uuid, toolchain) } override fun suggestToolchains(): List { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt new file mode 100644 index 00000000..ba80d0f7 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt @@ -0,0 +1,87 @@ +/* + * 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 . + */ + +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.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.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.ide.progress.TaskCancellation +import com.intellij.platform.ide.progress.withModalProgress +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 java.awt.Component +import javax.swing.DefaultComboBoxModel + +object Downloader { + suspend fun openDownloadDialog(component: Component) { + val info = withModalProgress( + component.let { ModalTaskOwner.component(it) }, + "Fetching zig version information", + TaskCancellation.Companion.cancellable()) { + ZigVersionInfo.Companion.download() + } + withEDTContext(ModalityState.stateForComponent(component)) { + val dialog = DialogBuilder() + val theList = ComboBox(DefaultComboBoxModel(info.map { it.first }.toTypedArray())) + val outputPath = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + ).also { + Disposer.register(dialog, it) + } + 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) + } + theList.addItemListener { + detect(it.item as String) + } + val center = panel { + row("Version:") { + cell(theList).resizableColumn().align(AlignX.FILL) + } + row("Location:") { + cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } + } + } + detect(info[0].first) + dialog.centerPanel(center) + dialog.setTitle("Version Selector") + dialog.addCancelAction() + dialog.showAndGet() + Disposer.dispose(dialog) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt new file mode 100644 index 00000000..c8de5969 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -0,0 +1,134 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +import com.intellij.openapi.Disposable +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.panel +import java.awt.event.ItemEvent +import javax.swing.JComponent +import kotlin.collections.addAll + +class ZigToolchainEditor(private val project: Project): Configurable { + private var myUi: UI? = null + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return "Zig" + } + + override fun createComponent(): JComponent? { + if (myUi != null) { + disposeUIResources() + } + val ui = UI() + myUi = ui + return panel { + ui.attach(this) + } + } + + override fun isModified(): Boolean { + return myUi?.isModified() == true + } + + override fun apply() { + myUi?.apply() + } + + override fun reset() { + myUi?.reset() + } + + override fun disposeUIResources() { + myUi?.let { Disposer.dispose(it) } + myUi = null + super.disposeUIResources() + } + + inner class UI(): Disposable { + private val toolchainBox: TCComboBox + private var oldSelectionIndex: Int = 0 + init { + val modelList = ArrayList() + 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)) + toolchainBox.addItemListener(::itemStateChanged) + reset() + } + + private fun itemStateChanged(event: ItemEvent) { + if (event.stateChange != ItemEvent.SELECTED) { + return + } + val item = event.item + if (item !is TCListElem) { + toolchainBox.selectedIndex = oldSelectionIndex + return + } + when(item) { + is TCListElem.None, is TCListElem.Toolchain.Actual -> { + oldSelectionIndex = toolchainBox.selectedIndex + } + else -> { + toolchainBox.selectedIndex = oldSelectionIndex + } + } + } + + + fun attach(p: Panel): Unit = with(p) { + row("Toolchain") { + cell(toolchainBox).resizableColumn().align(AlignX.FILL) + } + } + + fun isModified(): Boolean { + return ZigToolchainService.Companion.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain + } + + fun apply() { + ZigToolchainService.Companion.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain + } + + fun reset() { + toolchainBox.selectedToolchain = ZigToolchainService.Companion.getInstance(project).toolchainUUID + oldSelectionIndex = toolchainBox.selectedIndex + } + + override fun dispose() { + + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt new file mode 100644 index 00000000..908acb78 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -0,0 +1,123 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable +import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +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 java.util.UUID +import javax.swing.JComponent +import javax.swing.tree.DefaultTreeModel + +class ZigToolchainListEditor() : MasterDetailsComponent() { + private var isTreeInitialized = false + private var myComponent: JComponent? = null + + override fun createComponent(): JComponent { + if (!isTreeInitialized) { + initTree() + isTreeInitialized = true + } + val comp = super.createComponent() + myComponent = comp + return comp + } + + override fun createActions(fromPopup: Boolean): List { + val add = object : DumbAwareAction({ "lmaoo" }, Presentation.NULL_STRING, IconUtil.addIcon) { + override fun actionPerformed(e: AnActionEvent) { + val modelList = ArrayList() + modelList.addAll(TCListElem.fetchGroup) + modelList.add(Separator("Detected toolchains", true)) + modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) + val model = TCModel.Companion(modelList) + val context = TCContext(null, model) + val popup = TCComboBoxPopup(context, null, ::onItemSelected) + popup.showInBestPositionFor(e.dataContext) + } + } + return listOf(add, MyDeleteAction()) + } + + override fun onItemDeleted(item: Any?) { + if (item is UUID) { + ZigToolchainListService.getInstance().removeToolchain(item) + } + super.onItemDeleted(item) + } + + private fun onItemSelected(elem: TCListElem) { + when (elem) { + 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!!) + } + } + is TCListElem.FromDisk -> {} + is TCListElem.None -> {} + } + } + + override fun reset() { + reloadTree() + super.reset() + } + + override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchains.empty") + + override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") + + private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { + val node = MyNode(toolchain.createNamedConfigurable(uuid)) + addNode(node, myRoot) + } + + private fun reloadTree() { + myRoot.removeAllChildren() + ZigToolchainListService.getInstance().toolchains.forEach { (uuid, toolchain) -> + addToolchain(uuid, toolchain) + } + (myTree.model as DefaultTreeModel).reload() + } + + override fun disposeUIResources() { + super.disposeUIResources() + myComponent = null + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt new file mode 100644 index 00000000..45045252 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt @@ -0,0 +1,55 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import java.util.UUID + + +internal sealed interface TCListElemIn + +internal sealed interface TCListElem : TCListElemIn { + sealed interface Toolchain : TCListElem { + val toolchain: ZigToolchain + + @JvmRecord + data class Suggested(override val toolchain: ZigToolchain): Toolchain + + @JvmRecord + data class Actual(val uuid: UUID, override val toolchain: ZigToolchain): Toolchain + } + object None: TCListElem + object Download : TCListElem + object FromDisk : TCListElem + + companion object { + val fetchGroup get() = listOf(Download, FromDisk) + } +} + +@JvmRecord +internal data class Separator(val text: String, val line: Boolean) : TCListElemIn + +internal fun Pair.asActual() = TCListElem.Toolchain.Actual(first, second) + +internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt new file mode 100644 index 00000000..3c834ca4 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -0,0 +1,215 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import ai.grazie.utils.attributes.value +import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.project.toolchain.base.render +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.CellRendererPanel +import com.intellij.ui.CollectionComboBoxModel +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.GroupHeaderSeparator +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.components.panels.OpaquePanel +import com.intellij.ui.popup.list.ComboBoxPopup +import com.intellij.util.Consumer +import com.intellij.util.ui.EmptyIcon +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import java.awt.BorderLayout +import java.awt.Component +import java.util.IdentityHashMap +import java.util.UUID +import javax.accessibility.AccessibleContext +import javax.swing.JList +import javax.swing.border.Border + +internal class TCComboBoxPopup( + context: TCContext, + selected: TCListElem?, + onItemSelected: Consumer, +) : ComboBoxPopup(context, selected, onItemSelected) + +internal class TCComboBox(model: TCModel): ComboBox(model) { + init { + setRenderer(TCCellRenderer({model})) + } + + var selectedToolchain: UUID? + set(value) { + if (value == null) { + selectedItem = TCListElem.None + return + } + for (i in 0.. item.uuid + else -> null + } + } +} + +internal class TCModel private constructor(elements: List, private val separators: Map) : CollectionComboBoxModel(elements) { + companion object { + operator fun invoke(input: List): TCModel { + val separators = IdentityHashMap() + var lastSeparator: Separator? = null + val elements = ArrayList() + input.forEach { + when (it) { + is TCListElem -> { + if (lastSeparator != null) { + separators[it] = lastSeparator + lastSeparator = null + } + elements.add(it) + } + + is Separator -> lastSeparator = it + } + } + val model = TCModel(elements, separators) + return model + } + } + + fun separatorAbove(elem: TCListElem) = separators[elem] +} + +internal class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { + override fun getProject(): Project? { + return project + } + + override fun getModel(): TCModel { + return model + } + + override fun getRenderer(): TCCellRenderer { + return TCCellRenderer(::getModel) + } +} + +internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer() { + + override fun getListCellRendererComponent( + list: JList?, + value: TCListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ): Component? { + val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent + val panel = object : CellRendererPanel(BorderLayout()) { + val myContext = component.accessibleContext + + override fun getAccessibleContext(): AccessibleContext? { + return myContext + } + + override fun setBorder(border: Border?) { + component.border = border + } + } + panel.add(component, BorderLayout.CENTER) + + component.isOpaque = true + list?.let { background = if (selected) it.selectionBackground else it.background } + + val model = getModel() + + if (index == -1) { + component.isOpaque = false + panel.isOpaque = false + return panel + } + + val separator = value?.let { model.separatorAbove(it) } + + if (separator != null) { + val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 + val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) + separatorComponent.isHideLine = !separator.line + separatorComponent.caption = separator.text.ifBlank { null } + val wrapper = OpaquePanel(BorderLayout()) + wrapper.add(separatorComponent, BorderLayout.CENTER) + list?.let { wrapper.background = it.background } + panel.add(wrapper, BorderLayout.NORTH) + } + + return panel + } + + override fun customizeCellRenderer( + list: JList, + value: TCListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + icon = EMPTY_ICON + when (value) { + is TCListElem.Toolchain -> { + icon = when(value) { + is TCListElem.Toolchain.Suggested -> AllIcons.General.Information + is TCListElem.Toolchain.Actual -> Icons.Zig + } + val toolchain = value.toolchain + toolchain.render(this) + } + + is TCListElem.Download -> { + icon = AllIcons.Actions.Download + append("Download Zig\u2026") + } + + is TCListElem.FromDisk -> { + icon = AllIcons.General.OpenDisk + append("Add Zig from disk\u2026") + } + + is TCListElem.None, null -> { + icon = AllIcons.General.BalloonError + append("", SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } +} + +private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt new file mode 100644 index 00000000..e5742ba5 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt @@ -0,0 +1,49 @@ +/* + * 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 . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui.popup + +import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.project.toolchain.base.render +import com.falsepattern.zigbrains.project.toolchain.ui.Separator +import com.falsepattern.zigbrains.project.toolchain.ui.TCListElem +import com.falsepattern.zigbrains.project.toolchain.ui.TCListElemIn +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.ui.CellRendererPanel +import com.intellij.ui.CollectionListModel +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.GroupHeaderSeparator +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.components.panels.OpaquePanel +import com.intellij.ui.popup.list.ComboBoxPopup +import com.intellij.util.Consumer +import com.intellij.util.ui.EmptyIcon +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import java.awt.BorderLayout +import java.awt.Component +import java.util.IdentityHashMap +import javax.accessibility.AccessibleContext +import javax.swing.JList +import javax.swing.border.Border + diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index 7d1247b8..8e51f2ab 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -140,13 +140,13 @@ />