From ab20a57e9edf1610d9173d25520ba70e30ea875f Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 16:55:30 +0200 Subject: [PATCH] abstract away UUID storage for LSP code sharing --- .../toolchain/ZigToolchainListService.kt | 139 ++----------- .../project/toolchain/ZigToolchainService.kt | 4 +- .../project/toolchain/base/ZigToolchain.kt | 16 +- .../base/ZigToolchainConfigurable.kt | 4 +- .../toolchain/base/ZigToolchainProvider.kt | 6 +- .../toolchain/downloader/LocalSelector.kt | 8 +- .../toolchain/local/LocalZigToolchain.kt | 12 +- .../ui/ZigToolchainComboBoxHandler.kt | 6 +- .../toolchain/ui/ZigToolchainEditor.kt | 20 +- .../toolchain/ui/ZigToolchainListEditor.kt | 47 ++++- .../zigbrains/shared/NamedObject.kt | 32 +++ .../zigbrains/shared/UUIDMapSerializable.kt | 192 ++++++++++++++++++ 12 files changed, 323 insertions(+), 163 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index 8db422bd..7358067e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -22,125 +22,31 @@ 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.resolve 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 kotlinx.coroutines.launch -import java.lang.ref.WeakReference -import java.util.UUID @Service(Service.Level.APP) @State( name = "ZigToolchainList", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainListService: SerializablePersistentStateComponent(State()), IZigToolchainListService { - private val changeListeners = ArrayList>() +class ZigToolchainListService: UUIDMapSerializable.Converting(MyState()), IZigToolchainListService { + 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> - 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() - 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() - 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 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( + data class MyState( @JvmField - val toolchains: Map = emptyMap(), + val toolchains: ToolchainStorage = emptyMap(), ) companion object { @@ -149,19 +55,8 @@ class ZigToolchainListService: SerializablePersistentStateComponent> - fun setToolchain(uuid: UUID, toolchain: ZigToolchain) - 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 withUniqueName(toolchain: T): T -} +sealed interface IZigToolchainListService: ChangeTrackingStorage, AccessibleStorage, IterableStorage + +private typealias ToolchainStorage = UUIDStorage diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt index a769cc1e..d8965bbc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -45,7 +45,7 @@ import java.util.UUID class ZigToolchainService(val project: Project): SerializablePersistentStateComponent(State()), IZigToolchainService { override var toolchainUUID: UUID? get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { - if (ZigToolchainListService.getInstance().hasToolchain(it)) { + if (it in zigToolchainList) { true } else { updateState { @@ -64,7 +64,7 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp } override val toolchain: ZigToolchain? - get() = toolchainUUID?.let { ZigToolchainListService.getInstance().getToolchain(it) } + get() = toolchainUUID?.let { zigToolchainList[it] } data class State( @JvmField diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt index 23aa9122..2769d274 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -23,8 +23,10 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool +import com.falsepattern.zigbrains.shared.NamedObject import com.intellij.execution.configurations.GeneralCommandLine 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.MapAnnotation 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) */ -interface ZigToolchain { +interface ZigToolchain: NamedObject { val zig: ZigCompilerTool get() = ZigCompilerTool(this) - val name: String? + fun getUserData(key: Key): T? + + /** + * Returned type must be the same class + */ + fun withUserData(key: Key, value: T?): ZigToolchain fun workingDirectory(project: Project? = null): Path? @@ -43,11 +50,6 @@ interface ZigToolchain { fun pathToExecutable(toolName: String, project: Project? = null): Path - /** - * Returned object must be the same class. - */ - fun withName(newName: String?): ZigToolchain - data class Ref( @JvmField @Attribute diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt index 617c5a73..0746c934 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -23,10 +23,10 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.NlsContexts import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.util.minimumWidth import java.util.UUID import javax.swing.JComponent @@ -36,7 +36,7 @@ abstract class ZigToolchainConfigurable( ): NamedConfigurable() { var toolchain: T = tc set(value) { - ZigToolchainListService.getInstance().setToolchain(uuid, value) + zigToolchainList[uuid] = value field = value } private var myView: ZigToolchainPanel? = null diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt index c18a3923..58667862 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -22,8 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base -import com.falsepattern.zigbrains.direnv.DirenvState -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key @@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flowOn import java.util.UUID +import kotlin.collections.none private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") @@ -70,7 +70,7 @@ fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<* @OptIn(ExperimentalCoroutinesApi::class) fun suggestZigToolchains(project: Project? = null, data: UserDataHolder = emptyData): Flow { - val existing = ZigToolchainListService.getInstance().toolchains.map { (_, tc) -> tc }.toList() + val existing = zigToolchainList.map { (_, tc) -> tc } return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } val suggestions = ext.suggestToolchains(project, data) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt index fde0e7da..93553933 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt @@ -27,10 +27,12 @@ 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.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.withUniqueName import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.icons.AllIcons import com.intellij.openapi.application.ModalityState @@ -78,9 +80,7 @@ object LocalSelector { errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") dialog.setOkActionEnabled(false) } else { - val existingToolchain = ZigToolchainListService - .getInstance() - .toolchains + val existingToolchain = zigToolchainList .mapNotNull { it.second as? LocalZigToolchain } .firstOrNull { it.location == tc.location } if (existingToolchain != null) { @@ -95,7 +95,7 @@ object LocalSelector { } } if (tc != null) { - tc = ZigToolchainListService.getInstance().withUniqueName(tc) + tc = zigToolchainList.withUniqueName(tc) } val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() name.emptyText.text = tc?.name ?: "" diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index f5dc9045..70015f5f 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -28,13 +28,23 @@ import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.util.Key import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull +import com.intellij.util.keyFMap.KeyFMap import java.nio.file.Path @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 getUserData(key: Key): T? { + return userData.get(key) + } + + override fun withUserData(key: Key, value: T?): LocalZigToolchain { + return copy(userData = if (value == null) userData.minus(key) else userData.plus(key, value)) + } + override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt index 3febf6bd..0b832a62 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt @@ -25,6 +25,8 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader 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 java.awt.Component import java.util.UUID @@ -32,8 +34,8 @@ import java.util.UUID internal object ZigToolchainComboBoxHandler { @RequiresBackgroundThread 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.FromDisk -> LocalSelector.browseFromDisk(context) - }?.let { ZigToolchainListService.getInstance().registerNewToolchain(it) } + }?.let { zigToolchainList.registerNew(it) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt index c30d8295..8d270c91 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -23,14 +23,13 @@ package com.falsepattern.zigbrains.project.toolchain.ui 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.toolchain.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable 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.coroutine.asContextElement 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.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.awt.event.ItemEvent @@ -55,16 +55,17 @@ import java.util.UUID import javax.swing.JButton import kotlin.collections.addAll -class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable, ToolchainListChangeListener, ZigProjectConfigurationProvider.UserDataListener { +class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable, ZigProjectConfigurationProvider.UserDataListener { private val toolchainBox: TCComboBox private var selectOnNextReload: UUID? = null private val model: TCModel private var editButton: JButton? = null + private val changeListener: StorageChangeListener = { this@ZigToolchainEditor.toolchainListChanged() } init { model = TCModel(getModelList(project, sharedState)) toolchainBox = TCComboBox(model) toolchainBox.addItemListener(::itemStateChanged) - ZigToolchainListService.getInstance().addChangeListener(this) + zigToolchainList.addChangeListener(changeListener) sharedState.addUserDataChangeListener(this) model.whenListChanged { if (toolchainBox.isPopupVisible) { @@ -89,13 +90,14 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: return zigCoroutineScope.launch(toolchainBox.asContextElement()) { val uuid = runCatching { ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) }.getOrNull() + delay(100) withEDTContext(toolchainBox.asContextElement()) { applyUUIDNowOrOnReload(uuid) } } } - override suspend fun toolchainListChanged() { + private suspend fun toolchainListChanged() { withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { val list = getModelList(project, sharedState) 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 -> zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) { 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 apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config) if (apply) { @@ -182,7 +184,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: } override fun dispose() { - ZigToolchainListService.getInstance().removeChangeListener(this) + zigToolchainList.removeChangeListener(changeListener) } 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 { val modelList = ArrayList() 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.addAll(TCListElem.fetchGroup) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index e5fc8e71..7c39a202 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -23,11 +23,12 @@ package com.falsepattern.zigbrains.project.toolchain.ui 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.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable 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.withEDTContext 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.util.IconUtil import com.intellij.util.asSafely +import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.launch import java.util.UUID import javax.swing.JComponent import javax.swing.tree.DefaultTreeModel -class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeListener { +class ZigToolchainListEditor : MasterDetailsComponent() { private var isTreeInitialized = false private var registered: Boolean = false + private var selectOnNextReload: UUID? = null + private val changeListener: StorageChangeListener = { this@ZigToolchainListEditor.toolchainListChanged() } override fun createComponent(): JComponent { if (!isTreeInitialized) { @@ -54,7 +58,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList isTreeInitialized = true } if (!registered) { - ZigToolchainListService.getInstance().addChangeListener(this) + zigToolchainList.addChangeListener(changeListener) registered = true } return super.createComponent() @@ -81,7 +85,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList override fun onItemDeleted(item: Any?) { if (item is UUID) { - ZigToolchainListService.getInstance().removeToolchain(item) + zigToolchainList.remove(item) } super.onItemDeleted(item) } @@ -93,7 +97,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem) if (uuid != null) { 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 disposeUIResources() { + super.disposeUIResources() + if (registered) { + zigToolchainList.removeChangeListener(changeListener) + } + } + private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { val node = MyNode(toolchain.createNamedConfigurable(uuid)) addNode(node, myRoot) @@ -116,23 +127,37 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList private fun reloadTree() { val currentSelection = selectedObject?.asSafely() myRoot.removeAllChildren() - ZigToolchainListService.getInstance().toolchains.forEach { (uuid, toolchain) -> + val onReload = selectOnNextReload + selectOnNextReload = null + var hasOnReload = false + zigToolchainList.forEach { (uuid, toolchain) -> addToolchain(uuid, toolchain) + if (uuid == onReload) { + hasOnReload = true + } } (myTree.model as DefaultTreeModel).reload() + if (hasOnReload) { + selectNodeInTree(onReload) + return + } currentSelection?.let { selectNodeInTree(it) } } - override fun disposeUIResources() { - super.disposeUIResources() - if (registered) { - ZigToolchainListService.getInstance().removeChangeListener(this) + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + selectNodeInTree(uuid) + val currentSelection = selectedObject?.asSafely() + if (uuid != null && uuid != currentSelection) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null } } - override suspend fun toolchainListChanged() { + private suspend fun toolchainListChanged() { withEDTContext(myWholePanel.asContextElement()) { reloadTree() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt new file mode 100644 index 00000000..3bb35b82 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt @@ -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 . + */ + +package com.falsepattern.zigbrains.shared + +interface NamedObject> { + val name: String? + + /** + * Returned object must be the exact same class as the called one. + */ + fun withName(newName: String?): T +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt new file mode 100644 index 00000000..afc40ea4 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt @@ -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 . + */ + +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 = Map + +abstract class UUIDMapSerializable(init: S): SerializablePersistentStateComponent(init), ChangeTrackingStorage { + private val changeListeners = ArrayList>() + + protected abstract fun getStorage(state: S): UUIDStorage + + protected abstract fun updateStorage(state: S, storage: UUIDStorage): 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() + 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() + 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(init: S): + UUIDMapSerializable(init), + AccessibleStorage, + IterableStorage + { + 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> { + 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(init: S): Converting(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 { + 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: Iterable> + +fun , T: R> IterableStorage.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 +} \ No newline at end of file