work work work
This commit is contained in:
parent
e737058cb5
commit
2c500d40a5
8 changed files with 412 additions and 81 deletions
|
@ -32,6 +32,7 @@ import com.intellij.openapi.util.KeyWithDefaultValue
|
||||||
import com.intellij.openapi.util.SystemInfo
|
import com.intellij.openapi.util.SystemInfo
|
||||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||||
import com.intellij.openapi.vfs.toNioPathOrNull
|
import com.intellij.openapi.vfs.toNioPathOrNull
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
|
@ -71,10 +72,16 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, val name
|
||||||
}
|
}
|
||||||
|
|
||||||
fun tryFromPath(path: Path): LocalZigToolchain? {
|
fun tryFromPath(path: Path): LocalZigToolchain? {
|
||||||
val tc = LocalZigToolchain(path)
|
var tc = LocalZigToolchain(path)
|
||||||
if (!tc.zig.fileValid()) {
|
if (!tc.zig.fileValid()) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
tc.zig
|
||||||
|
.getEnvBlocking(null)
|
||||||
|
.getOrNull()
|
||||||
|
?.version
|
||||||
|
?.let { "Zig $it" }
|
||||||
|
?.let { tc = tc.copy(name = it) }
|
||||||
return tc
|
return tc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,13 @@ import com.intellij.openapi.projectRoots.Sdk
|
||||||
import com.intellij.openapi.roots.ui.configuration.SdkPopupBuilder
|
import com.intellij.openapi.roots.ui.configuration.SdkPopupBuilder
|
||||||
import com.intellij.openapi.ui.NamedConfigurable
|
import com.intellij.openapi.ui.NamedConfigurable
|
||||||
import com.intellij.openapi.util.UserDataHolder
|
import com.intellij.openapi.util.UserDataHolder
|
||||||
|
import com.intellij.openapi.util.io.FileUtil
|
||||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||||
|
import com.intellij.openapi.util.text.StringUtil
|
||||||
|
import com.intellij.ui.SimpleColoredComponent
|
||||||
|
import com.intellij.ui.SimpleTextAttributes
|
||||||
import com.intellij.util.EnvironmentUtil
|
import com.intellij.util.EnvironmentUtil
|
||||||
|
import com.intellij.util.IconUtil
|
||||||
import com.intellij.util.system.OS
|
import com.intellij.util.system.OS
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
@ -98,4 +103,24 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
|
||||||
EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) }
|
EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) }
|
||||||
return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) }
|
return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun render(toolchain: AbstractZigToolchain, component: SimpleColoredComponent) {
|
||||||
|
toolchain as LocalZigToolchain
|
||||||
|
component.append(presentDetectedPath(toolchain.location.pathString))
|
||||||
|
if (toolchain.name != null) {
|
||||||
|
component.append(" ")
|
||||||
|
component.append(toolchain.name, SimpleTextAttributes.GRAYED_ATTRIBUTES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun presentDetectedPath(home: String, maxLength: Int = 50, suffixLength: Int = 30): String {
|
||||||
|
//for macOS, let's try removing Bundle internals
|
||||||
|
var home = home
|
||||||
|
home = StringUtil.trimEnd(home, "/Contents/Home") //NON-NLS
|
||||||
|
home = StringUtil.trimEnd(home, "/Contents/MacOS") //NON-NLS
|
||||||
|
home = FileUtil.getLocationRelativeToUserHome(home, false)
|
||||||
|
home = StringUtil.shortenTextWithEllipsis(home, maxLength, suffixLength)
|
||||||
|
return home
|
||||||
}
|
}
|
|
@ -22,111 +22,94 @@
|
||||||
|
|
||||||
package com.falsepattern.zigbrains.project.toolchain
|
package com.falsepattern.zigbrains.project.toolchain
|
||||||
|
|
||||||
|
import com.falsepattern.zigbrains.Icons
|
||||||
import com.falsepattern.zigbrains.ZigBrainsBundle
|
import com.falsepattern.zigbrains.ZigBrainsBundle
|
||||||
import com.intellij.ide.projectView.TreeStructureProvider
|
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
|
||||||
import com.intellij.ide.util.treeView.AbstractTreeStructure
|
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
||||||
import com.intellij.ide.util.treeView.AbstractTreeStructureBase
|
import com.intellij.icons.AllIcons
|
||||||
import com.intellij.openapi.actionSystem.AnAction
|
import com.intellij.openapi.actionSystem.AnAction
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
import com.intellij.openapi.actionSystem.Presentation
|
import com.intellij.openapi.actionSystem.Presentation
|
||||||
|
import com.intellij.openapi.application.ModalityState
|
||||||
|
import com.intellij.openapi.application.PathManager
|
||||||
|
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||||
|
import com.intellij.openapi.progress.coroutineToIndicator
|
||||||
import com.intellij.openapi.project.DumbAwareAction
|
import com.intellij.openapi.project.DumbAwareAction
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
import com.intellij.openapi.project.ProjectManager
|
import com.intellij.openapi.project.ProjectManager
|
||||||
import com.intellij.openapi.roots.ui.SdkAppearanceService
|
import com.intellij.openapi.ui.ComboBox
|
||||||
import com.intellij.openapi.roots.ui.configuration.SdkListPresenter
|
import com.intellij.openapi.ui.DialogBuilder
|
||||||
import com.intellij.openapi.roots.ui.configuration.SdkPopupFactory
|
|
||||||
import com.intellij.openapi.ui.MasterDetailsComponent
|
import com.intellij.openapi.ui.MasterDetailsComponent
|
||||||
import com.intellij.openapi.ui.popup.JBPopupFactory
|
import com.intellij.openapi.util.Disposer
|
||||||
import com.intellij.openapi.ui.popup.util.BaseTreePopupStep
|
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||||
import com.intellij.openapi.util.NlsContexts
|
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||||
import com.intellij.ui.CollectionListModel
|
import com.intellij.platform.ide.progress.TaskCancellation
|
||||||
import com.intellij.ui.ColoredListCellRenderer
|
import com.intellij.platform.ide.progress.withModalProgress
|
||||||
import com.intellij.ui.SimpleTextAttributes
|
import com.intellij.platform.util.progress.withProgressText
|
||||||
import com.intellij.ui.components.JBLabel
|
import com.intellij.ui.*
|
||||||
import com.intellij.ui.components.JBPanel
|
import com.intellij.ui.components.JBList
|
||||||
import com.intellij.ui.components.panels.HorizontalLayout
|
import com.intellij.ui.components.panels.OpaquePanel
|
||||||
|
import com.intellij.ui.components.textFieldWithBrowseButton
|
||||||
|
import com.intellij.ui.dsl.builder.Align
|
||||||
|
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.dsl.builder.panel
|
||||||
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
|
|
||||||
import com.intellij.ui.popup.list.ComboBoxPopup
|
import com.intellij.ui.popup.list.ComboBoxPopup
|
||||||
import com.intellij.ui.treeStructure.SimpleNode
|
import com.intellij.util.Consumer
|
||||||
import com.intellij.ui.treeStructure.SimpleTreeStructure
|
|
||||||
import com.intellij.util.IconUtil
|
import com.intellij.util.IconUtil
|
||||||
|
import com.intellij.util.download.DownloadableFileService
|
||||||
|
import com.intellij.util.system.CpuArch
|
||||||
|
import com.intellij.util.text.SemVer
|
||||||
import com.intellij.util.ui.EmptyIcon
|
import com.intellij.util.ui.EmptyIcon
|
||||||
import com.intellij.util.ui.UIUtil.FontColor
|
import com.intellij.util.ui.JBUI
|
||||||
|
import com.intellij.util.ui.UIUtil
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
|
import java.awt.BorderLayout
|
||||||
import java.awt.Component
|
import java.awt.Component
|
||||||
import java.awt.LayoutManager
|
import java.nio.file.Path
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.function.Consumer
|
import javax.accessibility.AccessibleContext
|
||||||
import javax.swing.AbstractListModel
|
import javax.swing.DefaultComboBoxModel
|
||||||
import javax.swing.BoxLayout
|
|
||||||
import javax.swing.DefaultListModel
|
|
||||||
import javax.swing.JComponent
|
import javax.swing.JComponent
|
||||||
import javax.swing.JList
|
import javax.swing.JList
|
||||||
import javax.swing.JPanel
|
import javax.swing.JPanel
|
||||||
import javax.swing.ListCellRenderer
|
import javax.swing.ListCellRenderer
|
||||||
import javax.swing.ListModel
|
import javax.swing.border.Border
|
||||||
import javax.swing.SwingConstants
|
import javax.swing.event.DocumentEvent
|
||||||
import javax.swing.tree.DefaultTreeModel
|
import javax.swing.tree.DefaultTreeModel
|
||||||
import kotlin.io.path.pathString
|
|
||||||
|
|
||||||
class ZigToolchainListEditor(): MasterDetailsComponent() {
|
class ZigToolchainListEditor() : MasterDetailsComponent() {
|
||||||
private var isTreeInitialized = false
|
private var isTreeInitialized = false
|
||||||
|
private var myComponent: JComponent? = null
|
||||||
|
|
||||||
override fun createComponent(): JComponent {
|
override fun createComponent(): JComponent {
|
||||||
if (!isTreeInitialized) {
|
if (!isTreeInitialized) {
|
||||||
initTree()
|
initTree()
|
||||||
isTreeInitialized = true
|
isTreeInitialized = true
|
||||||
}
|
}
|
||||||
return super.createComponent()
|
val comp = super.createComponent()
|
||||||
|
myComponent = comp
|
||||||
|
return comp
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToolchainContext(private val project: Project?, private val model: ListModel<Any>): ComboBoxPopup.Context<Any> {
|
override fun createActions(fromPopup: Boolean): List<AnAction> {
|
||||||
override fun getProject(): Project? {
|
val add = object : DumbAwareAction({ "lmaoo" }, Presentation.NULL_STRING, IconUtil.addIcon) {
|
||||||
return project
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getModel(): ListModel<Any> {
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getRenderer(): ListCellRenderer<in Any> {
|
|
||||||
return object: ColoredListCellRenderer<Any>() {
|
|
||||||
override fun customizeCellRenderer(
|
|
||||||
list: JList<out Any?>,
|
|
||||||
value: Any?,
|
|
||||||
index: Int,
|
|
||||||
selected: Boolean,
|
|
||||||
hasFocus: Boolean
|
|
||||||
) {
|
|
||||||
icon = EMPTY_ICON
|
|
||||||
if (value is LocalZigToolchain) {
|
|
||||||
icon = IconUtil.addIcon
|
|
||||||
append(SdkListPresenter.presentDetectedSdkPath(value.location.pathString))
|
|
||||||
if (value.name != null) {
|
|
||||||
append(" ")
|
|
||||||
append(value.name, SimpleTextAttributes.GRAYED_ATTRIBUTES)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class ToolchainPopup(context: ToolchainContext,
|
|
||||||
selected: Any?,
|
|
||||||
onItemSelected: Consumer<Any>
|
|
||||||
): ComboBoxPopup<Any>(context, selected, onItemSelected) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createActions(fromPopup: Boolean): List<AnAction?>? {
|
|
||||||
val add = object : DumbAwareAction({"lmaoo"}, Presentation.NULL_STRING, IconUtil.addIcon) {
|
|
||||||
override fun actionPerformed(e: AnActionEvent) {
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
val toolchains = suggestZigToolchains(zigToolchainList.toolchains.map { it.second }.toList())
|
val toolchains = suggestZigToolchains(zigToolchainList.toolchains.map { it.second }.toList())
|
||||||
val final = ArrayList<Any>()
|
val final = ArrayList<TCListElemIn>()
|
||||||
final.addAll(toolchains)
|
final.add(TCListElem.Download)
|
||||||
val popup = ToolchainPopup(ToolchainContext(null, CollectionListModel(final)), null, {})
|
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)
|
popup.showInBestPositionFor(e.dataContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -140,6 +123,61 @@ class ZigToolchainListEditor(): MasterDetailsComponent() {
|
||||||
super.onItemDeleted(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() {
|
override fun reset() {
|
||||||
reloadTree()
|
reloadTree()
|
||||||
super.reset()
|
super.reset()
|
||||||
|
@ -149,20 +187,160 @@ class ZigToolchainListEditor(): MasterDetailsComponent() {
|
||||||
|
|
||||||
override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title")
|
override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title")
|
||||||
|
|
||||||
private fun addLocalToolchain(uuid: UUID, toolchain: LocalZigToolchain) {
|
private fun addToolchain(uuid: UUID, toolchain: AbstractZigToolchain) {
|
||||||
val node = MyNode(LocalZigToolchainConfigurable(uuid, toolchain, ProjectManager.getInstance().defaultProject))
|
val node = MyNode(toolchain.createNamedConfigurable(uuid, ProjectManager.getInstance().defaultProject))
|
||||||
addNode(node, myRoot)
|
addNode(node, myRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reloadTree() {
|
private fun reloadTree() {
|
||||||
myRoot.removeAllChildren()
|
myRoot.removeAllChildren()
|
||||||
zigToolchainList.toolchains.forEach { (uuid, toolchain) ->
|
zigToolchainList.toolchains.forEach { (uuid, toolchain) ->
|
||||||
if (toolchain is LocalZigToolchain) {
|
addToolchain(uuid, toolchain)
|
||||||
addLocalToolchain(uuid, toolchain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(myTree.model as DefaultTreeModel).reload()
|
(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: AbstractZigToolchain) : 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)
|
private val EMPTY_ICON = EmptyIcon.create(1, 16)
|
|
@ -63,7 +63,6 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
|
||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
@JvmField
|
@JvmField
|
||||||
@MapAnnotation(surroundKeyWithTag = false, surroundValueWithTag = false)
|
|
||||||
val toolchains: Map<String, AbstractZigToolchain.Ref> = emptyMap(),
|
val toolchains: Map<String, AbstractZigToolchain.Ref> = emptyMap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ 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
|
||||||
import com.intellij.openapi.util.UserDataHolder
|
import com.intellij.openapi.util.UserDataHolder
|
||||||
|
import com.intellij.ui.SimpleColoredComponent
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.asFlow
|
import kotlinx.coroutines.flow.asFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
@ -46,6 +47,7 @@ sealed interface ZigToolchainProvider {
|
||||||
fun matchesSuggestion(toolchain: AbstractZigToolchain, suggestion: AbstractZigToolchain): Boolean
|
fun matchesSuggestion(toolchain: AbstractZigToolchain, suggestion: AbstractZigToolchain): Boolean
|
||||||
fun createConfigurable(uuid: UUID, toolchain: AbstractZigToolchain, project: Project): NamedConfigurable<UUID>
|
fun createConfigurable(uuid: UUID, toolchain: AbstractZigToolchain, project: Project): NamedConfigurable<UUID>
|
||||||
fun suggestToolchains(): List<AbstractZigToolchain>
|
fun suggestToolchains(): List<AbstractZigToolchain>
|
||||||
|
fun render(toolchain: AbstractZigToolchain, component: SimpleColoredComponent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun AbstractZigToolchain.Ref.resolve(): AbstractZigToolchain? {
|
fun AbstractZigToolchain.Ref.resolve(): AbstractZigToolchain? {
|
||||||
|
@ -76,3 +78,8 @@ fun suggestZigToolchains(existing: List<AbstractZigToolchain>): List<AbstractZig
|
||||||
suggestions.filter { suggestion -> compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } }
|
suggestions.filter { suggestion -> compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun AbstractZigToolchain.render(component: SimpleColoredComponent) {
|
||||||
|
val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException()
|
||||||
|
return provider.render(this, component)
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* 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,6 +25,7 @@ package com.falsepattern.zigbrains.project.toolchain.tools
|
||||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||||
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSerializable
|
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSerializable
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
|
@ -45,6 +46,8 @@ class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) {
|
||||||
Result.failure(IllegalStateException("could not deserialize zig env", e))
|
Result.failure(IllegalStateException("could not deserialize zig env", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getEnvBlocking(project: Project?) = runBlocking { getEnv(project) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val envJson = Json {
|
private val envJson = Json {
|
||||||
|
|
|
@ -49,6 +49,12 @@ suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend Cor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun <T, R> T.letBlocking(targetAction: suspend CoroutineScope.(T) -> R): R {
|
||||||
|
return runBlocking {
|
||||||
|
targetAction(this@letBlocking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend inline fun <T> runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T {
|
suspend inline fun <T> runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T {
|
||||||
return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction)
|
return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue