project-level toolchain selector
This commit is contained in:
parent
9541bb9752
commit
8bb4e8bef1
20 changed files with 1016 additions and 538 deletions
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<AnAction> {
|
|
||||||
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<TCListElemIn>()
|
|
||||||
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<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)
|
|
||||||
}
|
|
||||||
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<TCListElem>,
|
|
||||||
) : ComboBoxPopup<TCListElem>(context, selected, onItemSelected)
|
|
||||||
|
|
||||||
private class TCModel private constructor(elements: List<TCListElem>, private val separators: Map<TCListElem, Separator>) : CollectionListModel<TCListElem>(elements) {
|
|
||||||
companion object {
|
|
||||||
operator fun invoke(input: List<TCListElemIn>): TCModel {
|
|
||||||
val separators = IdentityHashMap<TCListElem, Separator>()
|
|
||||||
var lastSeparator: Separator? = null
|
|
||||||
val elements = ArrayList<TCListElem>()
|
|
||||||
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<TCListElem> {
|
|
||||||
override fun getProject(): Project? {
|
|
||||||
return project
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getModel(): TCModel {
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRenderer(): ListCellRenderer<in TCListElem> {
|
|
||||||
return TCCellRenderer(::getModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer<TCListElem>() {
|
|
||||||
|
|
||||||
override fun getListCellRendererComponent(
|
|
||||||
list: JList<out TCListElem?>?,
|
|
||||||
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<out TCListElem?>,
|
|
||||||
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)
|
|
|
@ -67,6 +67,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
|
||||||
@JvmField
|
@JvmField
|
||||||
val toolchains: Map<String, ZigToolchain.Ref> = emptyMap(),
|
val toolchains: Map<String, ZigToolchain.Ref> = emptyMap(),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
val zigToolchainList get() = service<ZigToolchainListService>()
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun getInstance(): ZigToolchainListService = service()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<ZigToolchainService.State>(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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<Pair<String, ZigVersionInfo>> {
|
|
||||||
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<JsonObject>(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info?.mapNotNull getVersions@{ (version, data) ->
|
|
||||||
data as? JsonObject ?: return@getVersions null
|
|
||||||
val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: ""
|
|
||||||
val docs = data["docs"]?.asSafely<JsonPrimitive>()?.content ?: ""
|
|
||||||
val notes = data["notes"]?.asSafely<JsonPrimitive>()?.content ?: ""
|
|
||||||
val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<Tarball>(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<Tarball>(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")
|
|
|
@ -25,13 +25,16 @@ package com.falsepattern.zigbrains.project.toolchain.base
|
||||||
import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool
|
import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool
|
||||||
import com.intellij.execution.configurations.GeneralCommandLine
|
import com.intellij.execution.configurations.GeneralCommandLine
|
||||||
import com.intellij.openapi.project.Project
|
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.Attribute
|
||||||
import com.intellij.util.xmlb.annotations.MapAnnotation
|
import com.intellij.util.xmlb.annotations.MapAnnotation
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
|
||||||
abstract class ZigToolchain {
|
abstract class ZigToolchain: UserDataHolderBase() {
|
||||||
val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) }
|
val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) }
|
||||||
|
|
||||||
|
abstract val name: String?
|
||||||
|
|
||||||
abstract fun workingDirectory(project: Project? = null): Path?
|
abstract fun workingDirectory(project: Project? = null): Path?
|
||||||
|
|
||||||
abstract suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine
|
abstract suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<T: ZigToolchain>(
|
||||||
|
val uuid: UUID,
|
||||||
|
tc: T
|
||||||
|
): NamedConfigurable<UUID>() {
|
||||||
|
var toolchain: T = tc
|
||||||
|
set(value) {
|
||||||
|
ZigToolchainListService.getInstance().setToolchain(uuid, value)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
private var myView: ZigToolchainPanel<T>? = null
|
||||||
|
|
||||||
|
abstract fun createPanel(): ZigToolchainPanel<T>
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<T: ZigToolchain>: 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)
|
||||||
|
}
|
|
@ -22,6 +22,7 @@
|
||||||
|
|
||||||
package com.falsepattern.zigbrains.project.toolchain.base
|
package com.falsepattern.zigbrains.project.toolchain.base
|
||||||
|
|
||||||
|
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
|
||||||
import com.intellij.openapi.extensions.ExtensionPointName
|
import com.intellij.openapi.extensions.ExtensionPointName
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.ui.NamedConfigurable
|
import com.intellij.openapi.ui.NamedConfigurable
|
||||||
|
@ -39,7 +40,7 @@ internal interface ZigToolchainProvider {
|
||||||
fun deserialize(data: Map<String, String>): ZigToolchain?
|
fun deserialize(data: Map<String, String>): ZigToolchain?
|
||||||
fun serialize(toolchain: ZigToolchain): Map<String, String>
|
fun serialize(toolchain: ZigToolchain): Map<String, String>
|
||||||
fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean
|
fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean
|
||||||
fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, project: Project): NamedConfigurable<UUID>
|
fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*>
|
||||||
fun suggestToolchains(): List<ZigToolchain>
|
fun suggestToolchains(): List<ZigToolchain>
|
||||||
fun render(toolchain: ZigToolchain, component: SimpleColoredComponent)
|
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) }
|
return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(this, extraData) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ZigToolchain.createNamedConfigurable(uuid: UUID, project: Project): NamedConfigurable<UUID> {
|
fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*> {
|
||||||
val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException()
|
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<ZigToolchain>): List<ZigToolchain> {
|
fun suggestZigToolchains(): List<ZigToolchain> {
|
||||||
|
val existing = ZigToolchainListService.getInstance().toolchains.map { (uuid, tc) -> tc }.toList()
|
||||||
return EXTENSION_POINT_NAME.extensionList.flatMap { ext ->
|
return EXTENSION_POINT_NAME.extensionList.flatMap { ext ->
|
||||||
val compatibleExisting = existing.filter { ext.isCompatible(it) }
|
val compatibleExisting = existing.filter { ext.isCompatible(it) }
|
||||||
val suggestions = ext.suggestToolchains()
|
val suggestions = ext.suggestToolchains()
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Pair<String, ZigVersionInfo>> {
|
||||||
|
return withProgressText("Fetching zig version information") {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
doDownload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<JsonPrimitive>()?.content ?: ""
|
||||||
|
val docs = data["docs"]?.asSafely<JsonPrimitive>()?.content ?: ""
|
||||||
|
val notes = data["notes"]?.asSafely<JsonPrimitive>()?.content ?: ""
|
||||||
|
val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<Tarball>(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<Tarball>(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")
|
|
@ -35,7 +35,7 @@ import com.intellij.openapi.util.io.toNioPathOrNull
|
||||||
import com.intellij.openapi.vfs.toNioPathOrNull
|
import com.intellij.openapi.vfs.toNioPathOrNull
|
||||||
import java.nio.file.Path
|
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? {
|
override fun workingDirectory(project: Project?): Path? {
|
||||||
return project?.guessProjectDir()?.toNioPathOrNull()
|
return project?.guessProjectDir()?.toNioPathOrNull()
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,75 +22,16 @@
|
||||||
|
|
||||||
package com.falsepattern.zigbrains.project.toolchain.local
|
package com.falsepattern.zigbrains.project.toolchain.local
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
|
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
|
||||||
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 java.util.UUID
|
import java.util.UUID
|
||||||
import javax.swing.JComponent
|
|
||||||
|
|
||||||
class LocalZigToolchainConfigurable(
|
class LocalZigToolchainConfigurable(
|
||||||
val uuid: UUID,
|
uuid: UUID,
|
||||||
toolchain: LocalZigToolchain,
|
toolchain: LocalZigToolchain
|
||||||
private val project: Project
|
): ZigToolchainConfigurable<LocalZigToolchain>(uuid, toolchain) {
|
||||||
): NamedConfigurable<UUID>() {
|
override fun createPanel() = LocalZigToolchainPanel()
|
||||||
var toolchain: LocalZigToolchain = toolchain
|
|
||||||
set(value) {
|
|
||||||
zigToolchainList.setToolchain(uuid, value)
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
private var myView: LocalZigToolchainPanel? = null
|
|
||||||
override fun setDisplayName(name: String?) {
|
override fun setDisplayName(name: String?) {
|
||||||
toolchain = toolchain.copy(name = name)
|
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -23,6 +23,7 @@
|
||||||
package com.falsepattern.zigbrains.project.toolchain.local
|
package com.falsepattern.zigbrains.project.toolchain.local
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.ZigBrainsBundle
|
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.coroutine.withEDTContext
|
||||||
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
||||||
import com.intellij.openapi.Disposable
|
import com.intellij.openapi.Disposable
|
||||||
|
@ -45,8 +46,7 @@ import kotlinx.coroutines.launch
|
||||||
import javax.swing.event.DocumentEvent
|
import javax.swing.event.DocumentEvent
|
||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
class LocalZigToolchainPanel() : Disposable {
|
class LocalZigToolchainPanel() : ZigToolchainPanel<LocalZigToolchain>() {
|
||||||
private val nameField = JBTextField()
|
|
||||||
private val pathToToolchain = textFieldWithBrowseButton(
|
private val pathToToolchain = textFieldWithBrowseButton(
|
||||||
null,
|
null,
|
||||||
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
|
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
|
||||||
|
@ -75,11 +75,8 @@ class LocalZigToolchainPanel() : Disposable {
|
||||||
).also { Disposer.register(this, it) }
|
).also { Disposer.register(this, it) }
|
||||||
private var debounce: Job? = null
|
private var debounce: Job? = null
|
||||||
|
|
||||||
fun attach(p: Panel): Unit = with(p) {
|
override fun attach(p: Panel): Unit = with(p) {
|
||||||
row("Name") {
|
super.attach(p)
|
||||||
cell(nameField).resizableColumn().align(AlignX.FILL)
|
|
||||||
}
|
|
||||||
separator()
|
|
||||||
row(ZigBrainsBundle.message("settings.project.label.toolchain")) {
|
row(ZigBrainsBundle.message("settings.project.label.toolchain")) {
|
||||||
cell(pathToToolchain).resizableColumn().align(AlignX.FILL)
|
cell(pathToToolchain).resizableColumn().align(AlignX.FILL)
|
||||||
}
|
}
|
||||||
|
@ -92,27 +89,23 @@ class LocalZigToolchainPanel() : Disposable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isModified(cfg: LocalZigToolchainConfigurable): Boolean {
|
override fun isModified(toolchain: LocalZigToolchain): Boolean {
|
||||||
val name = nameField.text.ifBlank { null } ?: return false
|
val name = nameFieldValue ?: return false
|
||||||
val tc = cfg.toolchain
|
|
||||||
val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false
|
val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false
|
||||||
val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null
|
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 {
|
override fun apply(toolchain: LocalZigToolchain): LocalZigToolchain? {
|
||||||
val tc = cfg.toolchain
|
val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return null
|
||||||
val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false
|
|
||||||
val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else 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 toolchain.copy(location = location, std = std, name = nameFieldValue ?: "")
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reset(cfg: LocalZigToolchainConfigurable) {
|
override fun reset(toolchain: LocalZigToolchain) {
|
||||||
nameField.text = cfg.displayName ?: ""
|
nameFieldValue = toolchain.name
|
||||||
val tc = cfg.toolchain
|
this.pathToToolchain.text = toolchain.location.pathString
|
||||||
this.pathToToolchain.text = tc.location.pathString
|
val std = toolchain.std
|
||||||
val std = tc.std
|
|
||||||
if (std != null) {
|
if (std != null) {
|
||||||
stdFieldOverride.isSelected = true
|
stdFieldOverride.isSelected = true
|
||||||
pathToStd.text = std.pathString
|
pathToStd.text = std.pathString
|
||||||
|
|
|
@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.direnv.DirenvCmd
|
||||||
import com.falsepattern.zigbrains.direnv.emptyEnv
|
import com.falsepattern.zigbrains.direnv.emptyEnv
|
||||||
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
|
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
|
||||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
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.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.ui.NamedConfigurable
|
import com.intellij.openapi.ui.NamedConfigurable
|
||||||
|
@ -85,11 +86,10 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
|
||||||
|
|
||||||
override fun createConfigurable(
|
override fun createConfigurable(
|
||||||
uuid: UUID,
|
uuid: UUID,
|
||||||
toolchain: ZigToolchain,
|
toolchain: ZigToolchain
|
||||||
project: Project
|
): ZigToolchainConfigurable<*> {
|
||||||
): NamedConfigurable<UUID> {
|
|
||||||
toolchain as LocalZigToolchain
|
toolchain as LocalZigToolchain
|
||||||
return LocalZigToolchainConfigurable(uuid, toolchain, project)
|
return LocalZigToolchainConfigurable(uuid, toolchain)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun suggestToolchains(): List<ZigToolchain> {
|
override fun suggestToolchains(): List<ZigToolchain> {
|
||||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.falsepattern.zigbrains.project.toolchain.ui
|
||||||
|
|
||||||
|
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
|
||||||
|
import com.falsepattern.zigbrains.project.toolchain.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<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))
|
||||||
|
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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<AnAction> {
|
||||||
|
val add = object : DumbAwareAction({ "lmaoo" }, Presentation.NULL_STRING, IconUtil.addIcon) {
|
||||||
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
|
val modelList = ArrayList<TCListElemIn>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<UUID, ZigToolchain>.asActual() = TCListElem.Toolchain.Actual(first, second)
|
||||||
|
|
||||||
|
internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this)
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<TCListElem>,
|
||||||
|
) : ComboBoxPopup<TCListElem>(context, selected, onItemSelected)
|
||||||
|
|
||||||
|
internal class TCComboBox(model: TCModel): ComboBox<TCListElem>(model) {
|
||||||
|
init {
|
||||||
|
setRenderer(TCCellRenderer({model}))
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedToolchain: UUID?
|
||||||
|
set(value) {
|
||||||
|
if (value == null) {
|
||||||
|
selectedItem = TCListElem.None
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (i in 0..<model.size) {
|
||||||
|
val element = model.getElementAt(i)
|
||||||
|
if (element is TCListElem.Toolchain.Actual) {
|
||||||
|
if (element.uuid == value) {
|
||||||
|
selectedIndex = i
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedItem = TCListElem.None
|
||||||
|
}
|
||||||
|
get() {
|
||||||
|
val item = selectedItem
|
||||||
|
return when(item) {
|
||||||
|
is TCListElem.Toolchain.Actual -> item.uuid
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TCModel private constructor(elements: List<TCListElem>, private val separators: Map<TCListElem, Separator>) : CollectionComboBoxModel<TCListElem>(elements) {
|
||||||
|
companion object {
|
||||||
|
operator fun invoke(input: List<TCListElemIn>): TCModel {
|
||||||
|
val separators = IdentityHashMap<TCListElem, Separator>()
|
||||||
|
var lastSeparator: Separator? = null
|
||||||
|
val elements = ArrayList<TCListElem>()
|
||||||
|
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<TCListElem> {
|
||||||
|
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<TCListElem>() {
|
||||||
|
|
||||||
|
override fun getListCellRendererComponent(
|
||||||
|
list: JList<out TCListElem?>?,
|
||||||
|
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<out TCListElem?>,
|
||||||
|
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("<No Toolchain>", SimpleTextAttributes.ERROR_ATTRIBUTES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val EMPTY_ICON = EmptyIcon.create(1, 16)
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
|
|
@ -140,13 +140,13 @@
|
||||||
/>
|
/>
|
||||||
<projectConfigurable
|
<projectConfigurable
|
||||||
parentId="language"
|
parentId="language"
|
||||||
instance="com.falsepattern.zigbrains.project.settings.ZigConfigurable"
|
instance="com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainEditor"
|
||||||
id="ZigConfigurable"
|
id="ZigConfigurable"
|
||||||
displayName="Zig"
|
displayName="Zig"
|
||||||
/>
|
/>
|
||||||
<applicationConfigurable
|
<applicationConfigurable
|
||||||
parentId="ZigConfigurable"
|
parentId="ZigConfigurable"
|
||||||
instance="com.falsepattern.zigbrains.project.toolchain.ZigToolchainListEditor"
|
instance="com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainListEditor"
|
||||||
id="ZigToolchainConfigurable"
|
id="ZigToolchainConfigurable"
|
||||||
displayName="Toolchain"
|
displayName="Toolchain"
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Add table
Reference in a new issue