abstract away UUID storage for LSP code sharing

This commit is contained in:
FalsePattern 2025-04-10 16:55:30 +02:00
parent 1725b189a4
commit ab20a57e9e
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
12 changed files with 323 additions and 163 deletions

View file

@ -22,125 +22,31 @@
package com.falsepattern.zigbrains.project.toolchain package com.falsepattern.zigbrains.project.toolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService.MyState
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.resolve import com.falsepattern.zigbrains.project.toolchain.base.resolve
import com.falsepattern.zigbrains.project.toolchain.base.toRef import com.falsepattern.zigbrains.project.toolchain.base.toRef
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.AccessibleStorage
import com.falsepattern.zigbrains.shared.ChangeTrackingStorage
import com.falsepattern.zigbrains.shared.IterableStorage
import com.falsepattern.zigbrains.shared.UUIDMapSerializable
import com.falsepattern.zigbrains.shared.UUIDStorage
import com.intellij.openapi.components.* import com.intellij.openapi.components.*
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import java.util.UUID
@Service(Service.Level.APP) @Service(Service.Level.APP)
@State( @State(
name = "ZigToolchainList", name = "ZigToolchainList",
storages = [Storage("zigbrains.xml")] storages = [Storage("zigbrains.xml")]
) )
class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchainListService.State>(State()), IZigToolchainListService { class ZigToolchainListService: UUIDMapSerializable.Converting<ZigToolchain, ZigToolchain.Ref, MyState>(MyState()), IZigToolchainListService {
private val changeListeners = ArrayList<WeakReference<ToolchainListChangeListener>>() override fun serialize(value: ZigToolchain) = value.toRef()
override fun deserialize(value: ZigToolchain.Ref) = value.resolve()
override fun getStorage(state: MyState) = state.toolchains
override fun updateStorage(state: MyState, storage: ToolchainStorage) = state.copy(toolchains = storage)
override val toolchains: Sequence<Pair<UUID, ZigToolchain>> data class MyState(
get() = state.toolchains
.asSequence()
.mapNotNull {
val uuid = UUID.fromString(it.key) ?: return@mapNotNull null
val tc = it.value.resolve() ?: return@mapNotNull null
uuid to tc
}
override fun setToolchain(uuid: UUID, toolchain: ZigToolchain) {
val str = uuid.toString()
val ref = toolchain.toRef()
updateState {
val newMap = HashMap<String, ZigToolchain.Ref>()
newMap.putAll(it.toolchains)
newMap[str] = ref
it.copy(toolchains = newMap)
}
notifyChanged()
}
override fun registerNewToolchain(toolchain: ZigToolchain): UUID {
val ref = toolchain.toRef()
var uuid = UUID.randomUUID()
updateState {
val newMap = HashMap<String, ZigToolchain.Ref>()
newMap.putAll(it.toolchains)
var uuidStr = uuid.toString()
while (newMap.containsKey(uuidStr)) {
uuid = UUID.randomUUID()
uuidStr = uuid.toString()
}
newMap[uuidStr] = ref
it.copy(toolchains = newMap)
}
notifyChanged()
return uuid
}
override fun getToolchain(uuid: UUID): ZigToolchain? {
return state.toolchains[uuid.toString()]?.resolve()
}
override fun hasToolchain(uuid: UUID): Boolean {
return state.toolchains.containsKey(uuid.toString())
}
override fun removeToolchain(uuid: UUID) {
val str = uuid.toString()
updateState {
it.copy(toolchains = it.toolchains.filter { it.key != str })
}
notifyChanged()
}
override fun addChangeListener(listener: ToolchainListChangeListener) {
synchronized(changeListeners) {
changeListeners.add(WeakReference(listener))
}
}
override fun removeChangeListener(listener: ToolchainListChangeListener) {
synchronized(changeListeners) {
changeListeners.removeIf {
val v = it.get()
v == null || v === listener
}
}
}
override fun <T: ZigToolchain> withUniqueName(toolchain: T): T {
val baseName = toolchain.name ?: ""
var index = 0
var currentName = baseName
while (toolchains.any { (_, existing) -> existing.name == currentName }) {
index++
currentName = "$baseName ($index)"
}
@Suppress("UNCHECKED_CAST")
return toolchain.withName(currentName) as T
}
private fun notifyChanged() {
synchronized(changeListeners) {
var i = 0
while (i < changeListeners.size) {
val v = changeListeners[i].get()
if (v == null) {
changeListeners.removeAt(i)
continue
}
zigCoroutineScope.launch {
v.toolchainListChanged()
}
i++
}
}
}
data class State(
@JvmField @JvmField
val toolchains: Map<String, ZigToolchain.Ref> = emptyMap(), val toolchains: ToolchainStorage = emptyMap(),
) )
companion object { companion object {
@ -149,19 +55,8 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
} }
} }
@FunctionalInterface inline val zigToolchainList: IZigToolchainListService get() = ZigToolchainListService.getInstance()
interface ToolchainListChangeListener {
suspend fun toolchainListChanged()
}
sealed interface IZigToolchainListService { sealed interface IZigToolchainListService: ChangeTrackingStorage, AccessibleStorage<ZigToolchain>, IterableStorage<ZigToolchain>
val toolchains: Sequence<Pair<UUID, ZigToolchain>>
fun setToolchain(uuid: UUID, toolchain: ZigToolchain) private typealias ToolchainStorage = UUIDStorage<ZigToolchain.Ref>
fun registerNewToolchain(toolchain: ZigToolchain): UUID
fun getToolchain(uuid: UUID): ZigToolchain?
fun hasToolchain(uuid: UUID): Boolean
fun removeToolchain(uuid: UUID)
fun addChangeListener(listener: ToolchainListChangeListener)
fun removeChangeListener(listener: ToolchainListChangeListener)
fun <T: ZigToolchain> withUniqueName(toolchain: T): T
}

View file

@ -45,7 +45,7 @@ import java.util.UUID
class ZigToolchainService(val project: Project): SerializablePersistentStateComponent<ZigToolchainService.State>(State()), IZigToolchainService { class ZigToolchainService(val project: Project): SerializablePersistentStateComponent<ZigToolchainService.State>(State()), IZigToolchainService {
override var toolchainUUID: UUID? override var toolchainUUID: UUID?
get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf {
if (ZigToolchainListService.getInstance().hasToolchain(it)) { if (it in zigToolchainList) {
true true
} else { } else {
updateState { updateState {
@ -64,7 +64,7 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp
} }
override val toolchain: ZigToolchain? override val toolchain: ZigToolchain?
get() = toolchainUUID?.let { ZigToolchainListService.getInstance().getToolchain(it) } get() = toolchainUUID?.let { zigToolchainList[it] }
data class State( data class State(
@JvmField @JvmField

View file

@ -23,8 +23,10 @@
package com.falsepattern.zigbrains.project.toolchain.base 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.falsepattern.zigbrains.shared.NamedObject
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.Key
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
@ -32,10 +34,15 @@ import java.nio.file.Path
/** /**
* These MUST be stateless and interchangeable! (e.g., immutable data class) * These MUST be stateless and interchangeable! (e.g., immutable data class)
*/ */
interface ZigToolchain { interface ZigToolchain: NamedObject<ZigToolchain> {
val zig: ZigCompilerTool get() = ZigCompilerTool(this) val zig: ZigCompilerTool get() = ZigCompilerTool(this)
val name: String? fun <T> getUserData(key: Key<T>): T?
/**
* Returned type must be the same class
*/
fun <T> withUserData(key: Key<T>, value: T?): ZigToolchain
fun workingDirectory(project: Project? = null): Path? fun workingDirectory(project: Project? = null): Path?
@ -43,11 +50,6 @@ interface ZigToolchain {
fun pathToExecutable(toolName: String, project: Project? = null): Path fun pathToExecutable(toolName: String, project: Project? = null): Path
/**
* Returned object must be the same class.
*/
fun withName(newName: String?): ZigToolchain
data class Ref( data class Ref(
@JvmField @JvmField
@Attribute @Attribute

View file

@ -23,10 +23,10 @@
package com.falsepattern.zigbrains.project.toolchain.base package com.falsepattern.zigbrains.project.toolchain.base
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.util.minimumWidth
import java.util.UUID import java.util.UUID
import javax.swing.JComponent import javax.swing.JComponent
@ -36,7 +36,7 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
): NamedConfigurable<UUID>() { ): NamedConfigurable<UUID>() {
var toolchain: T = tc var toolchain: T = tc
set(value) { set(value) {
ZigToolchainListService.getInstance().setToolchain(uuid, value) zigToolchainList[uuid] = value
field = value field = value
} }
private var myView: ZigToolchainPanel<T>? = null private var myView: ZigToolchainPanel<T>? = null

View file

@ -22,8 +22,7 @@
package com.falsepattern.zigbrains.project.toolchain.base package com.falsepattern.zigbrains.project.toolchain.base
import com.falsepattern.zigbrains.direnv.DirenvState import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
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.util.Key import com.intellij.openapi.util.Key
@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import java.util.UUID import java.util.UUID
import kotlin.collections.none
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainProvider>("com.falsepattern.zigbrains.toolchainProvider") private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainProvider>("com.falsepattern.zigbrains.toolchainProvider")
@ -70,7 +70,7 @@ fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun suggestZigToolchains(project: Project? = null, data: UserDataHolder = emptyData): Flow<ZigToolchain> { fun suggestZigToolchains(project: Project? = null, data: UserDataHolder = emptyData): Flow<ZigToolchain> {
val existing = ZigToolchainListService.getInstance().toolchains.map { (_, tc) -> tc }.toList() val existing = zigToolchainList.map { (_, tc) -> tc }
return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext -> return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext ->
val compatibleExisting = existing.filter { ext.isCompatible(it) } val compatibleExisting = existing.filter { ext.isCompatible(it) }
val suggestions = ext.suggestToolchains(project, data) val suggestions = ext.suggestToolchains(project, data)

View file

@ -27,10 +27,12 @@ import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.withUniqueName
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.icons.AllIcons import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ModalityState
@ -78,9 +80,7 @@ object LocalSelector {
errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid")
dialog.setOkActionEnabled(false) dialog.setOkActionEnabled(false)
} else { } else {
val existingToolchain = ZigToolchainListService val existingToolchain = zigToolchainList
.getInstance()
.toolchains
.mapNotNull { it.second as? LocalZigToolchain } .mapNotNull { it.second as? LocalZigToolchain }
.firstOrNull { it.location == tc.location } .firstOrNull { it.location == tc.location }
if (existingToolchain != null) { if (existingToolchain != null) {
@ -95,7 +95,7 @@ object LocalSelector {
} }
} }
if (tc != null) { if (tc != null) {
tc = ZigToolchainListService.getInstance().withUniqueName(tc) tc = zigToolchainList.withUniqueName(tc)
} }
val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank()
name.emptyText.text = tc?.name ?: "" name.emptyText.text = tc?.name ?: ""

View file

@ -28,13 +28,23 @@ import com.intellij.execution.ExecutionException
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.project.guessProjectDir import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.Key
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 com.intellij.util.keyFMap.KeyFMap
import java.nio.file.Path import java.nio.file.Path
@JvmRecord @JvmRecord
data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain { data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null, private val userData: KeyFMap = KeyFMap.EMPTY_MAP): ZigToolchain {
override fun <T> getUserData(key: Key<T>): T? {
return userData.get(key)
}
override fun <T> withUserData(key: Key<T>, value: T?): LocalZigToolchain {
return copy(userData = if (value == null) userData.minus(key) else userData.plus(key, value))
}
override fun workingDirectory(project: Project?): Path? { override fun workingDirectory(project: Project?): Path? {
return project?.guessProjectDir()?.toNioPathOrNull() return project?.guessProjectDir()?.toNioPathOrNull()
} }

View file

@ -25,6 +25,8 @@ package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader
import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.withUniqueName
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import java.awt.Component import java.awt.Component
import java.util.UUID import java.util.UUID
@ -32,8 +34,8 @@ import java.util.UUID
internal object ZigToolchainComboBoxHandler { internal object ZigToolchainComboBoxHandler {
@RequiresBackgroundThread @RequiresBackgroundThread
suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) { suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) {
is TCListElem.Toolchain.Suggested -> ZigToolchainListService.getInstance().withUniqueName(elem.toolchain) is TCListElem.Toolchain.Suggested -> zigToolchainList.withUniqueName(elem.toolchain)
is TCListElem.Download -> Downloader.downloadToolchain(context) is TCListElem.Download -> Downloader.downloadToolchain(context)
is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context) is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context)
}?.let { ZigToolchainListService.getInstance().registerNewToolchain(it) } }?.let { zigToolchainList.registerNew(it) }
} }

View file

@ -23,14 +23,13 @@
package com.falsepattern.zigbrains.project.toolchain.ui package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.direnv.DirenvState
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.StorageChangeListener
import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.SubConfigurable
import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
@ -48,6 +47,7 @@ import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.awt.event.ItemEvent import java.awt.event.ItemEvent
@ -55,16 +55,17 @@ import java.util.UUID
import javax.swing.JButton import javax.swing.JButton
import kotlin.collections.addAll import kotlin.collections.addAll
class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>, ToolchainListChangeListener, ZigProjectConfigurationProvider.UserDataListener { class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>, ZigProjectConfigurationProvider.UserDataListener {
private val toolchainBox: TCComboBox private val toolchainBox: TCComboBox
private var selectOnNextReload: UUID? = null private var selectOnNextReload: UUID? = null
private val model: TCModel private val model: TCModel
private var editButton: JButton? = null private var editButton: JButton? = null
private val changeListener: StorageChangeListener = { this@ZigToolchainEditor.toolchainListChanged() }
init { init {
model = TCModel(getModelList(project, sharedState)) model = TCModel(getModelList(project, sharedState))
toolchainBox = TCComboBox(model) toolchainBox = TCComboBox(model)
toolchainBox.addItemListener(::itemStateChanged) toolchainBox.addItemListener(::itemStateChanged)
ZigToolchainListService.getInstance().addChangeListener(this) zigToolchainList.addChangeListener(changeListener)
sharedState.addUserDataChangeListener(this) sharedState.addUserDataChangeListener(this)
model.whenListChanged { model.whenListChanged {
if (toolchainBox.isPopupVisible) { if (toolchainBox.isPopupVisible) {
@ -89,13 +90,14 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
return return
zigCoroutineScope.launch(toolchainBox.asContextElement()) { zigCoroutineScope.launch(toolchainBox.asContextElement()) {
val uuid = runCatching { ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) }.getOrNull() val uuid = runCatching { ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) }.getOrNull()
delay(100)
withEDTContext(toolchainBox.asContextElement()) { withEDTContext(toolchainBox.asContextElement()) {
applyUUIDNowOrOnReload(uuid) applyUUIDNowOrOnReload(uuid)
} }
} }
} }
override suspend fun toolchainListChanged() { private suspend fun toolchainListChanged() {
withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { withContext(Dispatchers.EDT + toolchainBox.asContextElement()) {
val list = getModelList(project, sharedState) val list = getModelList(project, sharedState)
model.updateContents(list) model.updateContents(list)
@ -143,7 +145,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e -> button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e ->
zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) { zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) {
var selectedUUID = toolchainBox.selectedToolchain ?: return@launchWithEDT var selectedUUID = toolchainBox.selectedToolchain ?: return@launchWithEDT
val toolchain = ZigToolchainListService.getInstance().getToolchain(selectedUUID) ?: return@launchWithEDT val toolchain = zigToolchainList[selectedUUID] ?: return@launchWithEDT
val config = toolchain.createNamedConfigurable(selectedUUID) val config = toolchain.createNamedConfigurable(selectedUUID)
val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config) val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config)
if (apply) { if (apply) {
@ -182,7 +184,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
} }
override fun dispose() { override fun dispose() {
ZigToolchainListService.getInstance().removeChangeListener(this) zigToolchainList.removeChangeListener(changeListener)
} }
override val newProjectBeforeInitSelector get() = true override val newProjectBeforeInitSelector get() = true
@ -199,7 +201,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
private fun getModelList(project: Project?, data: UserDataHolder): List<TCListElemIn> { private fun getModelList(project: Project?, data: UserDataHolder): List<TCListElemIn> {
val modelList = ArrayList<TCListElemIn>() val modelList = ArrayList<TCListElemIn>()
modelList.add(TCListElem.None) modelList.add(TCListElem.None)
modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }.sortedBy { it.toolchain.name }) modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.toolchain.name })
modelList.add(Separator("", true)) modelList.add(Separator("", true))
modelList.addAll(TCListElem.fetchGroup) modelList.addAll(TCListElem.fetchGroup)
modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true))

View file

@ -23,11 +23,12 @@
package com.falsepattern.zigbrains.project.toolchain.ui package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.StorageChangeListener
import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.asContextElement
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
@ -39,14 +40,17 @@ import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.openapi.ui.MasterDetailsComponent
import com.intellij.util.IconUtil import com.intellij.util.IconUtil
import com.intellij.util.asSafely import com.intellij.util.asSafely
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID import java.util.UUID
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.tree.DefaultTreeModel import javax.swing.tree.DefaultTreeModel
class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeListener { class ZigToolchainListEditor : MasterDetailsComponent() {
private var isTreeInitialized = false private var isTreeInitialized = false
private var registered: Boolean = false private var registered: Boolean = false
private var selectOnNextReload: UUID? = null
private val changeListener: StorageChangeListener = { this@ZigToolchainListEditor.toolchainListChanged() }
override fun createComponent(): JComponent { override fun createComponent(): JComponent {
if (!isTreeInitialized) { if (!isTreeInitialized) {
@ -54,7 +58,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList
isTreeInitialized = true isTreeInitialized = true
} }
if (!registered) { if (!registered) {
ZigToolchainListService.getInstance().addChangeListener(this) zigToolchainList.addChangeListener(changeListener)
registered = true registered = true
} }
return super.createComponent() return super.createComponent()
@ -81,7 +85,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList
override fun onItemDeleted(item: Any?) { override fun onItemDeleted(item: Any?) {
if (item is UUID) { if (item is UUID) {
ZigToolchainListService.getInstance().removeToolchain(item) zigToolchainList.remove(item)
} }
super.onItemDeleted(item) super.onItemDeleted(item)
} }
@ -93,7 +97,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList
val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem) val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem)
if (uuid != null) { if (uuid != null) {
withEDTContext(myWholePanel.asContextElement()) { withEDTContext(myWholePanel.asContextElement()) {
selectNodeInTree(uuid) applyUUIDNowOrOnReload(uuid)
} }
} }
} }
@ -108,6 +112,13 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList
override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title") override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title")
override fun disposeUIResources() {
super.disposeUIResources()
if (registered) {
zigToolchainList.removeChangeListener(changeListener)
}
}
private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) {
val node = MyNode(toolchain.createNamedConfigurable(uuid)) val node = MyNode(toolchain.createNamedConfigurable(uuid))
addNode(node, myRoot) addNode(node, myRoot)
@ -116,23 +127,37 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList
private fun reloadTree() { private fun reloadTree() {
val currentSelection = selectedObject?.asSafely<UUID>() val currentSelection = selectedObject?.asSafely<UUID>()
myRoot.removeAllChildren() myRoot.removeAllChildren()
ZigToolchainListService.getInstance().toolchains.forEach { (uuid, toolchain) -> val onReload = selectOnNextReload
selectOnNextReload = null
var hasOnReload = false
zigToolchainList.forEach { (uuid, toolchain) ->
addToolchain(uuid, toolchain) addToolchain(uuid, toolchain)
if (uuid == onReload) {
hasOnReload = true
}
} }
(myTree.model as DefaultTreeModel).reload() (myTree.model as DefaultTreeModel).reload()
if (hasOnReload) {
selectNodeInTree(onReload)
return
}
currentSelection?.let { currentSelection?.let {
selectNodeInTree(it) selectNodeInTree(it)
} }
} }
override fun disposeUIResources() { @RequiresEdt
super.disposeUIResources() private fun applyUUIDNowOrOnReload(uuid: UUID?) {
if (registered) { selectNodeInTree(uuid)
ZigToolchainListService.getInstance().removeChangeListener(this) val currentSelection = selectedObject?.asSafely<UUID>()
if (uuid != null && uuid != currentSelection) {
selectOnNextReload = uuid
} else {
selectOnNextReload = null
} }
} }
override suspend fun toolchainListChanged() { private suspend fun toolchainListChanged() {
withEDTContext(myWholePanel.asContextElement()) { withEDTContext(myWholePanel.asContextElement()) {
reloadTree() reloadTree()
} }

View file

@ -0,0 +1,32 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.shared
interface NamedObject<T: NamedObject<T>> {
val name: String?
/**
* Returned object must be the exact same class as the called one.
*/
fun withName(newName: String?): T
}

View file

@ -0,0 +1,192 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.shared
import com.intellij.openapi.components.SerializablePersistentStateComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import java.util.UUID
import kotlin.collections.any
typealias UUIDStorage<T> = Map<String, T>
abstract class UUIDMapSerializable<T, S: Any>(init: S): SerializablePersistentStateComponent<S>(init), ChangeTrackingStorage {
private val changeListeners = ArrayList<WeakReference<StorageChangeListener>>()
protected abstract fun getStorage(state: S): UUIDStorage<T>
protected abstract fun updateStorage(state: S, storage: UUIDStorage<T>): S
override fun addChangeListener(listener: StorageChangeListener) {
synchronized(changeListeners) {
changeListeners.add(WeakReference(listener))
}
}
override fun removeChangeListener(listener: StorageChangeListener) {
synchronized(changeListeners) {
changeListeners.removeIf {
val v = it.get()
v == null || v === listener
}
}
}
protected fun registerNewUUID(value: T): UUID {
var uuid = UUID.randomUUID()
updateState {
val newMap = HashMap<String, T>()
newMap.putAll(getStorage(it))
var uuidStr = uuid.toString()
while (newMap.containsKey(uuidStr)) {
uuid = UUID.randomUUID()
uuidStr = uuid.toString()
}
newMap[uuidStr] = value
updateStorage(it, newMap)
}
notifyChanged()
return uuid
}
protected fun setStateUUID(uuid: UUID, value: T) {
val str = uuid.toString()
updateState {
val newMap = HashMap<String, T>()
newMap.putAll(getStorage(it))
newMap[str] = value
updateStorage(it, newMap)
}
notifyChanged()
}
protected fun getStateUUID(uuid: UUID): T? {
return getStorage(state)[uuid.toString()]
}
protected fun hasStateUUID(uuid: UUID): Boolean {
return getStorage(state).containsKey(uuid.toString())
}
protected fun removeStateUUID(uuid: UUID) {
val str = uuid.toString()
updateState {
updateStorage(state, getStorage(state).filter { it.key != str })
}
notifyChanged()
}
private fun notifyChanged() {
synchronized(changeListeners) {
var i = 0
while (i < changeListeners.size) {
val v = changeListeners[i].get()
if (v == null) {
changeListeners.removeAt(i)
continue
}
zigCoroutineScope.launch {
v()
}
i++
}
}
}
abstract class Converting<R, T, S: Any>(init: S):
UUIDMapSerializable<T, S>(init),
AccessibleStorage<R>,
IterableStorage<R>
{
protected abstract fun serialize(value: R): T
protected abstract fun deserialize(value: T): R?
override fun registerNew(value: R): UUID {
val ser = serialize(value)
return registerNewUUID(ser)
}
override operator fun set(uuid: UUID, value: R) {
val ser = serialize(value)
setStateUUID(uuid, ser)
}
override operator fun get(uuid: UUID): R? {
return getStateUUID(uuid)?.let { deserialize(it) }
}
override operator fun contains(uuid: UUID): Boolean {
return hasStateUUID(uuid)
}
override fun remove(uuid: UUID) {
removeStateUUID(uuid)
}
override fun iterator(): Iterator<Pair<UUID, R>> {
return getStorage(state)
.asSequence()
.mapNotNull {
val uuid = UUID.fromString(it.key) ?: return@mapNotNull null
val tc = deserialize(it.value) ?: return@mapNotNull null
uuid to tc
}.iterator()
}
}
abstract class Direct<T, S: Any>(init: S): Converting<T, T, S>(init) {
override fun serialize(value: T): T {
return value
}
override fun deserialize(value: T): T? {
return value
}
}
}
typealias StorageChangeListener = suspend CoroutineScope.() -> Unit
interface ChangeTrackingStorage {
fun addChangeListener(listener: StorageChangeListener)
fun removeChangeListener(listener: StorageChangeListener)
}
interface AccessibleStorage<R> {
fun registerNew(value: R): UUID
operator fun set(uuid: UUID, value: R)
operator fun get(uuid: UUID): R?
operator fun contains(uuid: UUID): Boolean
fun remove(uuid: UUID)
}
interface IterableStorage<R>: Iterable<Pair<UUID, R>>
fun <R: NamedObject<R>, T: R> IterableStorage<R>.withUniqueName(value: T): T {
val baseName = value.name ?: ""
var index = 0
var currentName = baseName
val names = this.map { (_, existing) -> existing.name }
while (names.any { it == currentName }) {
index++
currentName = "$baseName ($index)"
}
@Suppress("UNCHECKED_CAST")
return value.withName(currentName) as T
}