diff --git a/build.gradle.kts b/build.gradle.kts index 04c2476b..9f120e29 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ allprojects { } } filter { -// includeModule("com.redhat.devtools.intellij", "lsp4ij") + includeModule("com.redhat.devtools.intellij", "lsp4ij") } } mavenCentral() @@ -104,12 +104,12 @@ dependencies { pluginVerifier() zipSigner() -// plugin(lsp4ijPluginString) + plugin(lsp4ijPluginString) } runtimeOnly(project(":core")) runtimeOnly(project(":cidr")) -// runtimeOnly(project(":lsp")) + runtimeOnly(project(":lsp")) } intellijPlatform { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt index e0231243..3aa15bb8 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt @@ -92,6 +92,6 @@ abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationP } override val index: Int - get() = 1 + get() = 100 } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt index c28926cf..4fc2a466 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt @@ -35,7 +35,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.util.NlsActions.ActionText import com.intellij.openapi.vfs.toNioPathOrNull -import com.intellij.platform.util.progress.reportRawProgress import org.jdom.Element import org.jetbrains.annotations.Nls diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt index 6ff153e6..57b53003 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt @@ -25,8 +25,6 @@ package com.falsepattern.zigbrains.project.settings import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.shared.SubConfigurable import com.intellij.openapi.project.Project -import com.intellij.openapi.util.NlsContexts -import com.intellij.openapi.util.UserDataHolderBase class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter() { override fun instantiate(): List> { 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 d8965bbc..6cf457fe 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 @@ -42,8 +42,8 @@ import java.util.UUID name = "ZigToolchain", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainService(val project: Project): SerializablePersistentStateComponent(State()), IZigToolchainService { - override var toolchainUUID: UUID? +class ZigToolchainService(private val project: Project): SerializablePersistentStateComponent(State()) { + var toolchainUUID: UUID? get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { if (it in zigToolchainList) { true @@ -63,7 +63,7 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp } } - override val toolchain: ZigToolchain? + val toolchain: ZigToolchain? get() = toolchainUUID?.let { zigToolchainList[it] } data class State( @@ -74,11 +74,6 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp companion object { @JvmStatic - fun getInstance(project: Project): IZigToolchainService = project.service() + fun getInstance(project: Project): ZigToolchainService = project.service() } -} - -sealed interface IZigToolchainService { - var toolchainUUID: UUID? - val toolchain: ZigToolchain? } \ No newline at end of file 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 9de48994..c39841f4 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 @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.NlsContexts @@ -38,14 +39,14 @@ abstract class ZigToolchainConfigurable( zigToolchainList[uuid] = value field = value } - private var myViews: List> = emptyList() + private var myViews: List> = emptyList() - abstract fun createPanel(): ZigToolchainPanel + abstract fun createPanel(): ImmutableElementPanel override fun createOptionsPanel(): JComponent? { var views = myViews if (views.isEmpty()) { - views = ArrayList>() + views = ArrayList>() views.add(createPanel()) views.addAll(createZigToolchainExtensionPanels()) views.forEach { it.reset(toolchain) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt index 75ba7c5e..e0be25d6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt @@ -22,16 +22,17 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.intellij.openapi.extensions.ExtensionPointName private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainExtensionsProvider") internal interface ZigToolchainExtensionsProvider { - fun createExtensionPanel(): ZigToolchainPanel? + fun createExtensionPanel(): ImmutableElementPanel? val index: Int } -fun createZigToolchainExtensionPanels(): List> { +fun createZigToolchainExtensionPanels(): List> { return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull { it.createExtensionPanel() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt index 6183b3a0..5a7522e9 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt @@ -49,7 +49,6 @@ import com.intellij.util.asSafely import com.intellij.util.concurrency.annotations.RequiresEdt import java.awt.Component import java.nio.file.Path -import java.util.* import javax.swing.DefaultComboBoxModel import javax.swing.JList import javax.swing.event.DocumentEvent diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt index 87e80f67..8b9d54b6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt @@ -50,10 +50,8 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromStream import java.io.File -import java.lang.IllegalStateException import java.nio.file.Files import java.nio.file.Path -import java.util.* import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.deleteRecursively import kotlin.io.path.isDirectory diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt index f5b4f612..85736d5e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt @@ -23,7 +23,7 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainPanelBase +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.application.ModalityState @@ -44,7 +44,7 @@ import kotlinx.coroutines.launch import javax.swing.event.DocumentEvent import kotlin.io.path.pathString -class LocalZigToolchainPanel() : ZigToolchainPanelBase() { +class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase() { private val pathToToolchain = textFieldWithBrowseButton( null, FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index 49b223d7..14a79acc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -23,7 +23,6 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.direnv.DirenvService -import com.falsepattern.zigbrains.direnv.DirenvState import com.falsepattern.zigbrains.direnv.Env import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt similarity index 82% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt index 109f9c1a..2737529e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt @@ -20,17 +20,17 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.base +package com.falsepattern.zigbrains.project.toolchain.ui import com.intellij.openapi.Disposable import com.intellij.ui.dsl.builder.Panel -interface ZigToolchainPanel: Disposable { +interface ImmutableElementPanel: Disposable { fun attach(p: Panel) - fun isModified(toolchain: T): Boolean + fun isModified(elem: T): Boolean /** * Returned object must be the exact same class as the provided one. */ - fun apply(toolchain: T): T? - fun reset(toolchain: T) + fun apply(elem: T): T? + fun reset(elem: T) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt similarity index 85% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt index 5d2543bd..d19571f6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt @@ -20,17 +20,15 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.base +package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle -import com.intellij.openapi.Disposable +import com.falsepattern.zigbrains.shared.NamedObject import com.intellij.ui.components.JBTextField import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.util.preferredHeight -import java.awt.Dimension -abstract class ZigToolchainPanelBase: ZigToolchainPanel { +abstract class ImmutableNamedElementPanelBase: ImmutableElementPanel { private val nameField = JBTextField(25) protected var nameFieldValue: String? 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 0b832a62..ffc5a025 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 @@ -22,10 +22,11 @@ package com.falsepattern.zigbrains.project.toolchain.ui -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain 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.ui.ListElem import com.falsepattern.zigbrains.shared.withUniqueName import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import java.awt.Component @@ -33,9 +34,9 @@ import java.util.UUID internal object ZigToolchainComboBoxHandler { @RequiresBackgroundThread - suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) { - is TCListElem.Toolchain.Suggested -> zigToolchainList.withUniqueName(elem.toolchain) - is TCListElem.Download -> Downloader.downloadToolchain(context) - is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context) + suspend fun onItemSelected(context: Component, elem: ListElem.Pseudo): UUID? = when(elem) { + is ListElem.One.Suggested -> zigToolchainList.withUniqueName(elem.instance) + is ListElem.Download -> Downloader.downloadToolchain(context) + is ListElem.FromDisk -> LocalSelector.browseFromDisk(context) }?.let { zigToolchainList.registerNew(it) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt new file mode 100644 index 00000000..f8188478 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt @@ -0,0 +1,92 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.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.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ListElemIn +import com.falsepattern.zigbrains.shared.ui.Separator +import com.falsepattern.zigbrains.shared.ui.UUIDComboBoxDriver +import com.falsepattern.zigbrains.shared.ui.ZBComboBox +import com.falsepattern.zigbrains.shared.ui.ZBContext +import com.falsepattern.zigbrains.shared.ui.ZBModel +import com.falsepattern.zigbrains.shared.ui.asActual +import com.falsepattern.zigbrains.shared.ui.asPending +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.UserDataHolder +import java.awt.Component +import java.util.UUID + +sealed interface ZigToolchainDriver: UUIDComboBoxDriver { + override val theMap get() = zigToolchainList + + override fun createContext(model: ZBModel): ZBContext { + return TCContext(null, model) + } + + override fun createComboBox(model: ZBModel): ZBComboBox { + return TCComboBox(model) + } + + override suspend fun resolvePseudo( + context: Component, + elem: ListElem.Pseudo + ): UUID? { + return ZigToolchainComboBoxHandler.onItemSelected(context, elem) + } + + override fun createNamedConfigurable( + uuid: UUID, + elem: ZigToolchain + ): NamedConfigurable { + return elem.createNamedConfigurable(uuid) + } + + object ForList: ZigToolchainDriver { + override fun constructModelList(): List> { + val modelList = ArrayList>() + modelList.addAll(ListElem.fetchGroup()) + modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) + modelList.add(suggestZigToolchains().asPending()) + return modelList + } + } + + class ForSelector(val project: Project?, val data: UserDataHolder): ZigToolchainDriver { + override fun constructModelList(): List> { + val modelList = ArrayList>() + modelList.add(ListElem.None()) + modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.instance.name }) + modelList.add(Separator("", true)) + modelList.addAll(ListElem.fetchGroup()) + modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) + modelList.add(suggestZigToolchains(project, data).asPending()) + return modelList + } + } +} \ 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 8d270c91..12ac6894 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 @@ -24,113 +24,29 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -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.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.SubConfigurable -import com.falsepattern.zigbrains.shared.coroutine.asContextElement -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.openapi.application.EDT -import com.intellij.openapi.observable.util.whenListChanged -import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.util.Key -import com.intellij.openapi.util.UserDataHolder -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 -import java.util.UUID -import javax.swing.JButton -import kotlin.collections.addAll -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() } +class ZigToolchainEditor(private var project: Project?, + private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): + UUIDMapSelector(ZigToolchainDriver.ForSelector(project, sharedState)), + SubConfigurable, + ZigProjectConfigurationProvider.UserDataListener +{ init { - model = TCModel(getModelList(project, sharedState)) - toolchainBox = TCComboBox(model) - toolchainBox.addItemListener(::itemStateChanged) - zigToolchainList.addChangeListener(changeListener) sharedState.addUserDataChangeListener(this) - model.whenListChanged { - if (toolchainBox.isPopupVisible) { - toolchainBox.isPopupVisible = false - toolchainBox.isPopupVisible = true - } - } - } - - private fun refreshButtonState(item: Any?) { - editButton?.isEnabled = item is TCListElem.Toolchain.Actual - editButton?.repaint() - } - - private fun itemStateChanged(event: ItemEvent) { - if (event.stateChange != ItemEvent.SELECTED) { - return - } - val item = event.item - refreshButtonState(item) - if (item !is TCListElem.Pseudo) - return - zigCoroutineScope.launch(toolchainBox.asContextElement()) { - val uuid = runCatching { ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) }.getOrNull() - delay(100) - withEDTContext(toolchainBox.asContextElement()) { - applyUUIDNowOrOnReload(uuid) - } - } - } - - private suspend fun toolchainListChanged() { - withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { - val list = getModelList(project, sharedState) - model.updateContents(list) - val onReload = selectOnNextReload - selectOnNextReload = null - if (onReload != null) { - val element = list.firstOrNull { when(it) { - is TCListElem.Toolchain.Actual -> it.uuid == onReload - else -> false - } } - model.selectedItem = element - return@withContext - } - val selected = model.selected - if (selected != null && list.contains(selected)) { - model.selectedItem = selected - return@withContext - } - if (selected is TCListElem.Toolchain.Actual) { - val uuid = selected.uuid - val element = list.firstOrNull { when(it) { - is TCListElem.Toolchain.Actual -> it.uuid == uuid - else -> false - } } - model.selectedItem = element - return@withContext - } - model.selectedItem = TCListElem.None - } } override fun onUserDataChanged(key: Key<*>) { - zigCoroutineScope.launch { toolchainListChanged() } + zigCoroutineScope.launch { listChanged() } } @@ -141,50 +57,26 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: else "settings.toolchain.editor.toolchain.label") ) { - cell(toolchainBox).resizableColumn().align(AlignX.FILL) - button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e -> - zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) { - var selectedUUID = toolchainBox.selectedToolchain ?: 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) { - applyUUIDNowOrOnReload(selectedUUID) - } - } - }.component.let { - editButton = it - refreshButtonState(toolchainBox.selectedItem) - } - } - } - - @RequiresEdt - private fun applyUUIDNowOrOnReload(uuid: UUID?) { - toolchainBox.selectedToolchain = uuid - if (uuid != null && toolchainBox.selectedToolchain == null) { - selectOnNextReload = uuid - } else { - selectOnNextReload = null + attachComboBoxRow(this) } } override fun isModified(context: Project): Boolean { - return ZigToolchainService.getInstance(context).toolchainUUID != toolchainBox.selectedToolchain + return ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID } override fun apply(context: Project) { - ZigToolchainService.getInstance(context).toolchainUUID = toolchainBox.selectedToolchain + ZigToolchainService.getInstance(context).toolchainUUID = selectedUUID } override fun reset(context: Project?) { val project = context ?: ProjectManager.getInstance().defaultProject - toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID - refreshButtonState(toolchainBox.selectedItem) + selectedUUID = ZigToolchainService.getInstance(project).toolchainUUID } override fun dispose() { - zigToolchainList.removeChangeListener(changeListener) + super.dispose() + sharedState.removeUserDataChangeListener(this) } override val newProjectBeforeInitSelector get() = true @@ -195,16 +87,4 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: override val index: Int get() = 0 } -} - - -private fun getModelList(project: Project?, data: UserDataHolder): List { - val modelList = ArrayList() - modelList.add(TCListElem.None) - 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)) - modelList.add(suggestZigToolchains(project, data).asPending()) - return modelList } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index 7c39a202..7dd9131d 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,143 +23,9 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains -import com.falsepattern.zigbrains.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 -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.Presentation -import com.intellij.openapi.observable.util.whenListChanged -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() { - 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) { - initTree() - isTreeInitialized = true - } - if (!registered) { - zigToolchainList.addChangeListener(changeListener) - registered = true - } - return super.createComponent() - } - - override fun createActions(fromPopup: Boolean): List { - val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.toolchain.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) { - override fun actionPerformed(e: AnActionEvent) { - val modelList = ArrayList() - modelList.addAll(TCListElem.fetchGroup) - modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) - modelList.add(suggestZigToolchains().asPending()) - val model = TCModel(modelList) - val context = TCContext(null, model) - val popup = TCComboBoxPopup(context, null, ::onItemSelected) - model.whenListChanged { - popup.syncWithModelChange() - } - popup.showInBestPositionFor(e.dataContext) - } - } - return listOf(add, MyDeleteAction()) - } - - override fun onItemDeleted(item: Any?) { - if (item is UUID) { - zigToolchainList.remove(item) - } - super.onItemDeleted(item) - } - - private fun onItemSelected(elem: TCListElem) { - if (elem !is TCListElem.Pseudo) - return - zigCoroutineScope.launch(myWholePanel.asContextElement()) { - val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem) - if (uuid != null) { - withEDTContext(myWholePanel.asContextElement()) { - applyUUIDNowOrOnReload(uuid) - } - } - } - } - - override fun reset() { - reloadTree() - super.reset() - } - - override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchain.list.empty") +import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +class ZigToolchainListEditor : UUIDMapEditor(ZigToolchainDriver.ForList) { 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) - } - - private fun reloadTree() { - val currentSelection = selectedObject?.asSafely() - myRoot.removeAllChildren() - 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) - } - } - - @RequiresEdt - private fun applyUUIDNowOrOnReload(uuid: UUID?) { - selectNodeInTree(uuid) - val currentSelection = selectedObject?.asSafely() - if (uuid != null && uuid != currentSelection) { - selectOnNextReload = uuid - } else { - selectOnNextReload = null - } - } - - private suspend fun toolchainListChanged() { - withEDTContext(myWholePanel.asContextElement()) { - reloadTree() - } - } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt deleted file mode 100644 index ab014123..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain.ui - -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import java.util.UUID - - -internal sealed interface TCListElemIn -internal sealed interface TCListElem : TCListElemIn { - sealed interface Pseudo: TCListElem - sealed interface Toolchain : TCListElem { - val toolchain: ZigToolchain - - @JvmRecord - data class Suggested(override val toolchain: ZigToolchain): Toolchain, Pseudo - - @JvmRecord - data class Actual(val uuid: UUID, override val toolchain: ZigToolchain): Toolchain - } - object None: TCListElem - object Download : TCListElem, Pseudo - object FromDisk : TCListElem, Pseudo - data class Pending(val elems: Flow): TCListElem - - companion object { - val fetchGroup get() = listOf(Download, FromDisk) - } -} - -@JvmRecord -internal data class Separator(val text: String, val line: Boolean) : TCListElemIn - -internal fun Pair.asActual() = TCListElem.Toolchain.Actual(first, second) - -internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this) - -internal fun Flow.asPending() = TCListElem.Pending(map { it.asSuggested() }) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index 2f490d13..8945a8d3 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -24,269 +24,61 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.render -import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer +import com.falsepattern.zigbrains.shared.ui.ZBComboBox +import com.falsepattern.zigbrains.shared.ui.ZBContext +import com.falsepattern.zigbrains.shared.ui.ZBModel import com.intellij.icons.AllIcons -import com.intellij.openapi.application.EDT -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.asContextElement -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.ComboBox -import com.intellij.ui.CellRendererPanel -import com.intellij.ui.CollectionComboBoxModel -import com.intellij.ui.ColoredListCellRenderer -import com.intellij.ui.GroupHeaderSeparator -import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes -import com.intellij.ui.components.panels.OpaquePanel -import com.intellij.ui.popup.list.ComboBoxPopup -import com.intellij.util.Consumer -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.ui.EmptyIcon -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.awt.BorderLayout -import java.awt.Component -import java.util.IdentityHashMap -import java.util.UUID -import javax.accessibility.AccessibleContext +import com.intellij.ui.icons.EMPTY_ICON import javax.swing.JList -import javax.swing.border.Border -internal class TCComboBoxPopup( - context: TCContext, - selected: TCListElem?, - onItemSelected: Consumer, -) : ComboBoxPopup(context, selected, onItemSelected) +class TCComboBox(model: ZBModel): ZBComboBox(model, ::TCCellRenderer) -internal class TCComboBox(model: TCModel): ComboBox(model) { - init { - setRenderer(TCCellRenderer { model }) - } - - var selectedToolchain: UUID? - set(value) { - if (value == null) { - selectedItem = TCListElem.None - return - } - for (i in 0.. item.uuid - else -> null - } - } -} - -internal class TCModel private constructor(elements: List, private var separators: MutableMap) : CollectionComboBoxModel(elements) { - private var counter: Int = 0 - companion object { - operator fun invoke(input: List): TCModel { - val (elements, separators) = convert(input) - val model = TCModel(elements, separators) - model.launchPendingResolve() - return model - } - - private fun convert(input: List): Pair, MutableMap> { - val separators = IdentityHashMap() - var lastSeparator: Separator? = null - val elements = ArrayList() - input.forEach { - when (it) { - is TCListElem -> { - if (lastSeparator != null) { - separators[it] = lastSeparator - lastSeparator = null - } - elements.add(it) - } - - is Separator -> lastSeparator = it - } - } - return elements to separators - } - } - - fun separatorAbove(elem: TCListElem) = separators[elem] - - private fun launchPendingResolve() { - runInEdt(ModalityState.any()) { - val counter = this.counter - val size = this.size - for (i in 0.. - insertBefore(elem, newElem, counter) - } - remove(elem, counter) - } - } - } - } - - @RequiresEdt - private fun remove(old: TCListElem, oldCounter: Int) { - val newCounter = this@TCModel.counter - if (oldCounter != newCounter) { - return - } - val index = this@TCModel.getElementIndex(old) - this@TCModel.remove(index) - val sep = separators.remove(old) - if (sep != null && this@TCModel.size > index) { - this@TCModel.getElementAt(index)?.let { separators[it] = sep } - } - } - - @RequiresEdt - private fun insertBefore(old: TCListElem, new: TCListElem?, oldCounter: Int) { - val newCounter = this@TCModel.counter - if (oldCounter != newCounter) { - return - } - if (new == null) { - return - } - val currentIndex = this@TCModel.getElementIndex(old) - separators.remove(old)?.let { - separators.put(new, it) - } - this@TCModel.add(currentIndex, new) - } - - @RequiresEdt - fun updateContents(input: List) { - counter++ - val (elements, separators) = convert(input) - this.separators = separators - replaceAll(elements) - launchPendingResolve() - } -} - -internal class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { - override fun getProject(): Project? { - return project - } - - override fun getModel(): TCModel { - return model - } - - override fun getRenderer(): TCCellRenderer { - return TCCellRenderer(::getModel) - } -} - -internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer() { - - override fun getListCellRendererComponent( - list: JList?, - value: TCListElem?, - index: Int, - selected: Boolean, - hasFocus: Boolean - ): Component? { - val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent - val panel = object : CellRendererPanel(BorderLayout()) { - val myContext = component.accessibleContext - - override fun getAccessibleContext(): AccessibleContext? { - return myContext - } - - override fun setBorder(border: Border?) { - component.border = border - } - } - panel.add(component, BorderLayout.CENTER) - - component.isOpaque = true - list?.let { background = if (selected) it.selectionBackground else it.background } - - val model = getModel() - - if (index == -1) { - component.isOpaque = false - panel.isOpaque = false - return panel - } - - val separator = value?.let { model.separatorAbove(it) } - - if (separator != null) { - val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 - val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) - separatorComponent.isHideLine = !separator.line - separatorComponent.caption = separator.text.ifBlank { null } - val wrapper = OpaquePanel(BorderLayout()) - wrapper.add(separatorComponent, BorderLayout.CENTER) - list?.let { wrapper.background = it.background } - panel.add(wrapper, BorderLayout.NORTH) - } - - return panel - } +class TCContext(project: Project?, model: ZBModel): ZBContext(project, model, ::TCCellRenderer) +class TCCellRenderer(getModel: () -> ZBModel): ZBCellRenderer(getModel) { override fun customizeCellRenderer( - list: JList, - value: TCListElem?, + list: JList?>, + value: ListElem?, index: Int, selected: Boolean, hasFocus: Boolean ) { icon = EMPTY_ICON when (value) { - is TCListElem.Toolchain -> { + is ListElem.One -> { val (icon, isSuggestion) = when(value) { - is TCListElem.Toolchain.Suggested -> AllIcons.General.Information to true - is TCListElem.Toolchain.Actual -> Icons.Zig to false + is ListElem.One.Suggested -> AllIcons.General.Information to true + is ListElem.One.Actual -> Icons.Zig to false } this.icon = icon - val toolchain = value.toolchain - toolchain.render(this, isSuggestion, index == -1) + val item = value.instance + item.render(this, isSuggestion, index == -1) } - is TCListElem.Download -> { + is ListElem.Download -> { icon = AllIcons.Actions.Download append(ZigBrainsBundle.message("settings.toolchain.model.download.text")) } - is TCListElem.FromDisk -> { + is ListElem.FromDisk -> { icon = AllIcons.General.OpenDisk append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text")) } - is TCListElem.Pending -> { + is ListElem.Pending -> { icon = AllIcons.Empty - append("Loading\u2026", SimpleTextAttributes.GRAYED_ATTRIBUTES) + append(ZigBrainsBundle.message("settings.toolchain.model.loading.text"), SimpleTextAttributes.GRAYED_ATTRIBUTES) } - is TCListElem.None, null -> { + is ListElem.None, null -> { icon = AllIcons.General.BalloonError append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) } } } -} -private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt index e1c48c16..d8042fae 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt @@ -28,7 +28,6 @@ import com.falsepattern.zigbrains.shared.ipc.IPCUtil import com.falsepattern.zigbrains.shared.ipc.ipc import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.openapi.options.ConfigurationException diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt new file mode 100644 index 00000000..7a735a6b --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt @@ -0,0 +1,37 @@ +/* + * 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.ui + +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.intellij.openapi.ui.NamedConfigurable +import java.awt.Component +import java.util.UUID + +interface UUIDComboBoxDriver { + val theMap: UUIDMapSerializable.Converting + fun constructModelList(): List> + fun createContext(model: ZBModel): ZBContext + fun createComboBox(model: ZBModel): ZBComboBox + suspend fun resolvePseudo(context: Component, elem: ListElem.Pseudo): UUID? + fun createNamedConfigurable(uuid: UUID, elem: T): NamedConfigurable +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt new file mode 100644 index 00000000..75eb7620 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt @@ -0,0 +1,155 @@ +/* + * 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.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +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 +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.observable.util.whenListChanged +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 + +abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetailsComponent() { + private var isTreeInitialized = false + private var registered: Boolean = false + private var selectOnNextReload: UUID? = null + private val changeListener: StorageChangeListener = { this@UUIDMapEditor.listChanged() } + + override fun createComponent(): JComponent { + if (!isTreeInitialized) { + initTree() + isTreeInitialized = true + } + if (!registered) { + driver.theMap.addChangeListener(changeListener) + registered = true + } + return super.createComponent() + } + + override fun createActions(fromPopup: Boolean): List { + val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.shared.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) { + override fun actionPerformed(e: AnActionEvent) { + val modelList = driver.constructModelList() + val model = ZBModel(modelList) + val context = driver.createContext(model) + val popup = ZBComboBoxPopup(context, null, ::onItemSelected) + model.whenListChanged { + popup.syncWithModelChange() + } + popup.showInBestPositionFor(e.dataContext) + } + } + return listOf(add, MyDeleteAction()) + } + + override fun onItemDeleted(item: Any?) { + if (item is UUID) { + driver.theMap.remove(item) + } + super.onItemDeleted(item) + } + + private fun onItemSelected(elem: ListElem) { + if (elem !is ListElem.Pseudo) + return + zigCoroutineScope.launch(myWholePanel.asContextElement()) { + val uuid = driver.resolvePseudo(myWholePanel, elem) + if (uuid != null) { + withEDTContext(myWholePanel.asContextElement()) { + applyUUIDNowOrOnReload(uuid) + } + } + } + } + + override fun reset() { + reloadTree() + super.reset() + } + + override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.shared.list.empty") + + override fun disposeUIResources() { + super.disposeUIResources() + if (registered) { + driver.theMap.removeChangeListener(changeListener) + } + } + + private fun addElem(uuid: UUID, elem: T) { + val node = MyNode(driver.createNamedConfigurable(uuid, elem)) + addNode(node, myRoot) + } + + private fun reloadTree() { + val currentSelection = selectedObject?.asSafely() + myRoot.removeAllChildren() + val onReload = selectOnNextReload + selectOnNextReload = null + var hasOnReload = false + driver.theMap.forEach { (uuid, elem) -> + addElem(uuid, elem) + if (uuid == onReload) { + hasOnReload = true + } + } + (myTree.model as DefaultTreeModel).reload() + if (hasOnReload) { + selectNodeInTree(onReload) + return + } + currentSelection?.let { + selectNodeInTree(it) + } + } + + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + selectNodeInTree(uuid) + val currentSelection = selectedObject?.asSafely() + if (uuid != null && uuid != currentSelection) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null + } + } + + private suspend fun listChanged() { + withEDTContext(myWholePanel.asContextElement()) { + reloadTree() + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt new file mode 100644 index 00000000..b77b94b1 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt @@ -0,0 +1,161 @@ +/* + * 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.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +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.launchWithEDT +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.EDT +import com.intellij.openapi.observable.util.whenListChanged +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Row +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 +import java.util.UUID +import javax.swing.JButton + +abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable { + private val comboBox: ZBComboBox + private var selectOnNextReload: UUID? = null + private val model: ZBModel + private var editButton: JButton? = null + private val changeListener: StorageChangeListener = { this@UUIDMapSelector.listChanged() } + init { + model = ZBModel(driver.constructModelList()) + comboBox = driver.createComboBox(model) + comboBox.addItemListener(::itemStateChanged) + driver.theMap.addChangeListener(changeListener) + model.whenListChanged { + if (comboBox.isPopupVisible) { + comboBox.isPopupVisible = false + comboBox.isPopupVisible = true + } + } + } + + protected var selectedUUID: UUID? + get() = comboBox.selectedUUID + set(value) { + comboBox.selectedUUID = value + refreshButtonState(value) + } + + private fun refreshButtonState(item: Any?) { + editButton?.isEnabled = item is ListElem.One.Actual<*> + editButton?.repaint() + } + + private fun itemStateChanged(event: ItemEvent) { + if (event.stateChange != ItemEvent.SELECTED) { + return + } + val item = event.item + refreshButtonState(item) + if (item !is ListElem.Pseudo<*>) + return + @Suppress("UNCHECKED_CAST") + item as ListElem.Pseudo + zigCoroutineScope.launch(comboBox.asContextElement()) { + val uuid = runCatching { driver.resolvePseudo(comboBox, item) }.getOrNull() + delay(100) + withEDTContext(comboBox.asContextElement()) { + applyUUIDNowOrOnReload(uuid) + } + } + } + + protected suspend fun listChanged() { + withContext(Dispatchers.EDT + comboBox.asContextElement()) { + val list = driver.constructModelList() + model.updateContents(list) + val onReload = selectOnNextReload + selectOnNextReload = null + if (onReload != null) { + val element = list.firstOrNull { when(it) { + is ListElem.One.Actual<*> -> it.uuid == onReload + else -> false + } } + model.selectedItem = element + return@withContext + } + val selected = model.selected + if (selected != null && list.contains(selected)) { + model.selectedItem = selected + return@withContext + } + if (selected is ListElem.One.Actual<*>) { + val uuid = selected.uuid + val element = list.firstOrNull { when(it) { + is ListElem.One.Actual -> it.uuid == uuid + else -> false + } } + model.selectedItem = element + return@withContext + } + model.selectedItem = ListElem.None() + } + } + + protected fun attachComboBoxRow(row: Row): Unit = with(row) { + cell(comboBox).resizableColumn().align(AlignX.FILL) + button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e -> + zigCoroutineScope.launchWithEDT(comboBox.asContextElement()) { + var selectedUUID = comboBox.selectedUUID ?: return@launchWithEDT + val elem = driver.theMap[selectedUUID] ?: return@launchWithEDT + val config = driver.createNamedConfigurable(selectedUUID, elem) + val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(comboBox)?.contentPane, config) + if (apply) { + applyUUIDNowOrOnReload(selectedUUID) + } + } + }.component.let { + editButton = it + refreshButtonState(comboBox.selectedItem) + } + } + + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + comboBox.selectedUUID = uuid + if (uuid != null && comboBox.selectedUUID == null) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null + } + } + + override fun dispose() { + zigToolchainList.removeChangeListener(changeListener) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt new file mode 100644 index 00000000..cd59bd9b --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt @@ -0,0 +1,82 @@ +/* + * 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.ui + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.UUID + + +sealed interface ListElemIn +@Suppress("UNCHECKED_CAST") +sealed interface ListElem : ListElemIn { + sealed interface Pseudo: ListElem + sealed interface One : ListElem { + val instance: T + + @JvmRecord + data class Suggested(override val instance: T): One, Pseudo + + @JvmRecord + data class Actual(val uuid: UUID, override val instance: T): One + } + class None private constructor(): ListElem { + companion object { + private val INSTANCE = None() + operator fun invoke(): None { + return INSTANCE as None + } + } + } + class Download private constructor(): ListElem, Pseudo { + companion object { + private val INSTANCE = Download() + operator fun invoke(): Download { + return INSTANCE as Download + } + } + } + class FromDisk private constructor(): ListElem, Pseudo { + companion object { + private val INSTANCE = FromDisk() + operator fun invoke(): FromDisk { + return INSTANCE as FromDisk + } + } + } + data class Pending(val elems: Flow>): ListElem + + companion object { + private val fetchGroup = listOf>(Download(), FromDisk()) + fun fetchGroup() = fetchGroup as List> + } +} + +@JvmRecord +data class Separator(val text: String, val line: Boolean) : ListElemIn + +fun Pair.asActual() = ListElem.One.Actual(first, second) + +fun T.asSuggested() = ListElem.One.Suggested(this) + +fun Flow.asPending() = ListElem.Pending(map { it.asSuggested() }) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt new file mode 100644 index 00000000..dbb8e82c --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt @@ -0,0 +1,255 @@ +/* + * 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.ui + +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.CellRendererPanel +import com.intellij.ui.CollectionComboBoxModel +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.GroupHeaderSeparator +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.components.panels.OpaquePanel +import com.intellij.ui.popup.list.ComboBoxPopup +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.EmptyIcon +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.awt.BorderLayout +import java.awt.Component +import java.util.IdentityHashMap +import java.util.UUID +import java.util.function.Consumer +import javax.accessibility.AccessibleContext +import javax.swing.JList +import javax.swing.border.Border + +class ZBComboBoxPopup( + context: ZBContext, + selected: ListElem?, + onItemSelected: Consumer>, +) : ComboBoxPopup>(context, selected, onItemSelected) + +open class ZBComboBox(model: ZBModel, renderer: (() -> ZBModel)-> ZBCellRenderer): ComboBox>(model) { + init { + setRenderer(renderer { model }) + } + + var selectedUUID: UUID? + set(value) { + if (value == null) { + selectedItem = ListElem.None + return + } + for (i in 0.. -> item.uuid + else -> null + } + } +} + +class ZBModel private constructor(elements: List>, private var separators: MutableMap, Separator>) : CollectionComboBoxModel>(elements) { + private var counter: Int = 0 + companion object { + operator fun invoke(input: List>): ZBModel { + val (elements, separators) = convert(input) + val model = ZBModel(elements, separators) + model.launchPendingResolve() + return model + } + + private fun convert(input: List>): Pair>, MutableMap, Separator>> { + val separators = IdentityHashMap, Separator>() + var lastSeparator: Separator? = null + val elements = ArrayList>() + input.forEach { + when (it) { + is ListElem -> { + if (lastSeparator != null) { + separators[it] = lastSeparator + lastSeparator = null + } + elements.add(it) + } + + is Separator -> lastSeparator = it + } + } + return elements to separators + } + } + + fun separatorAbove(elem: ListElem) = separators[elem] + + private fun launchPendingResolve() { + runInEdt(ModalityState.any()) { + val counter = this.counter + val size = this.size + for (i in 0.. + insertBefore(elem, newElem, counter) + } + remove(elem, counter) + } + } + } + } + + @RequiresEdt + private fun remove(old: ListElem, oldCounter: Int) { + val newCounter = this@ZBModel.counter + if (oldCounter != newCounter) { + return + } + val index = this@ZBModel.getElementIndex(old) + this@ZBModel.remove(index) + val sep = separators.remove(old) + if (sep != null && this@ZBModel.size > index) { + this@ZBModel.getElementAt(index)?.let { separators[it] = sep } + } + } + + @RequiresEdt + private fun insertBefore(old: ListElem, new: ListElem?, oldCounter: Int) { + val newCounter = this@ZBModel.counter + if (oldCounter != newCounter) { + return + } + if (new == null) { + return + } + val currentIndex = this@ZBModel.getElementIndex(old) + separators.remove(old)?.let { + separators.put(new, it) + } + this@ZBModel.add(currentIndex, new) + } + + @RequiresEdt + fun updateContents(input: List>) { + counter++ + val (elements, separators) = convert(input) + this.separators = separators + replaceAll(elements) + launchPendingResolve() + } +} + +open class ZBContext(private val project: Project?, private val model: ZBModel, private val getRenderer: (() -> ZBModel) -> ZBCellRenderer) : ComboBoxPopup.Context> { + override fun getProject(): Project? { + return project + } + + override fun getModel(): ZBModel { + return model + } + + override fun getRenderer(): ZBCellRenderer { + return getRenderer(::getModel) + } +} + +abstract class ZBCellRenderer(val getModel: () -> ZBModel) : ColoredListCellRenderer>() { + final override fun getListCellRendererComponent( + list: JList?>?, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ): Component? { + val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent + val panel = object : CellRendererPanel(BorderLayout()) { + val myContext = component.accessibleContext + + override fun getAccessibleContext(): AccessibleContext? { + return myContext + } + + override fun setBorder(border: Border?) { + component.border = border + } + } + panel.add(component, BorderLayout.CENTER) + + component.isOpaque = true + list?.let { background = if (selected) it.selectionBackground else it.background } + + val model = getModel() + + if (index == -1) { + component.isOpaque = false + panel.isOpaque = false + return panel + } + + val separator = value?.let { model.separatorAbove(it) } + + if (separator != null) { + val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 + val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) + separatorComponent.isHideLine = !separator.line + separatorComponent.caption = separator.text.ifBlank { null } + val wrapper = OpaquePanel(BorderLayout()) + wrapper.add(separatorComponent, BorderLayout.CENTER) + list?.let { wrapper.background = it.background } + panel.add(wrapper, BorderLayout.NORTH) + } + + return panel + } + + abstract override fun customizeCellRenderer( + list: JList?>, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) +} + +private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index d81ebb97..3de98cc2 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -110,22 +110,22 @@ build.tool.window.status.error.general=Error while running zig build -l build.tool.window.status.no-builds=No builds currently in progress build.tool.window.status.timeout=zig build -l timed out after {0} seconds. zig=Zig +settings.shared.list.add-action.name=Add New +settings.shared.list.empty=Select an entry to view or edit its details here settings.project.display-name=Zig settings.toolchain.base.name.label=Name settings.toolchain.local.path.label=Toolchain location settings.toolchain.local.version.label=Detected zig version settings.toolchain.local.std.label=Override standard library -settings.toolchain.editor.display-name=Zig settings.toolchain.editor.toolchain.label=Toolchain settings.toolchain.editor.toolchain-default.label=Default toolchain settings.toolchain.editor.toolchain.edit-button.name=Edit settings.toolchain.model.detected.separator=Detected toolchains settings.toolchain.model.none.text= +settings.toolchain.model.loading.text=Loading\u2026 settings.toolchain.model.from-disk.text=Add Zig from disk\u2026 settings.toolchain.model.download.text=Download Zig\u2026 settings.toolchain.list.title=Toolchains -settings.toolchain.list.add-action.name=Add New -settings.toolchain.list.empty=Select a toolchain to view or edit its details here settings.toolchain.downloader.title=Install Zig settings.toolchain.downloader.version.label=Version: settings.toolchain.downloader.location.label=Location: diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt index ff8a9837..300dd6cd 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt @@ -24,20 +24,18 @@ package com.falsepattern.zigbrains.lsp import com.falsepattern.zigbrains.lsp.config.SuspendingZLSConfigProvider import com.falsepattern.zigbrains.lsp.config.ZLSConfig -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project -import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.util.io.toNioPathOrNull import kotlin.io.path.pathString class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider { override suspend fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { - val svc = project.zigProjectSettings - var state = svc.state - val toolchain = state.toolchain ?: project.suggestZigToolchain(UserDataHolderBase()) ?: return previous + val svc = ZigToolchainService.getInstance(project) + val toolchain = svc.toolchain ?: return previous val env = toolchain.zig.getEnv(project).getOrElse { throwable -> throwable.printStackTrace() @@ -65,16 +63,10 @@ class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider { ).notify(project) return previous } - var lib = if (state.overrideStdPath && state.explicitPathToStd != null) { - state.explicitPathToStd?.toNioPathOrNull() ?: run { - Notification( - "zigbrains-lsp", - "Invalid zig standard library path override: ${state.explicitPathToStd}", - NotificationType.ERROR - ).notify(project) - null - } - } else null + var lib = if (toolchain is LocalZigToolchain) + toolchain.std + else + null if (lib == null) { lib = env.libDirectory.toNioPathOrNull() ?: run { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt deleted file mode 100644 index 63228b94..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp - -import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsConfigurable -import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsPanel -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager - -class ZLSProjectConfigurationProvider: ZigProjectConfigurationProvider { - override fun handleMainConfigChanged(project: Project) { - startLSP(project, true) - } - - override fun createConfigurable(project: Project): SubConfigurable { - return ZLSSettingsConfigurable(project) - } - - override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel { - return ZLSSettingsPanel(ProjectManager.getInstance().defaultProject) - } - - override val priority: Int - get() = 1000 -} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt index d197dd30..7f4ccc86 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt @@ -22,36 +22,19 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity import com.intellij.ui.EditorNotifications import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlin.io.path.pathString class ZLSStartup: ProjectActivity { override suspend fun execute(project: Project) { - val zlsState = project.zlsSettings.state - if (zlsState.zlsPath.isBlank()) { - val env = if (DirenvCmd.direnvInstalled() && !project.isDefault && project.zigProjectSettings.state.direnv) - project.getDirenv() - else - emptyEnv - env.findExecutableOnPATH("zls")?.let { - zlsState.zlsPath = it.pathString - project.zlsSettings.state = zlsState - } - } project.zigCoroutineScope.launch { - var currentState = project.zlsRunningAsync() + var currentState = project.zlsRunning() while (!project.isDisposed) { - val running = project.zlsRunningAsync() + val running = project.zlsRunning() if (currentState != running) { EditorNotifications.getInstance(project).updateAllNotifications() } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt index 6470d863..5f499b0e 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt @@ -22,11 +22,8 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv import com.falsepattern.zigbrains.lsp.config.ZLSConfigProviderBase -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.lsp.zls.ZLSService import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -55,30 +52,9 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje @OptIn(ExperimentalSerializationApi::class) suspend fun getCommand(project: Project): List? { - val svc = project.zlsSettings - val state = svc.state - val zlsPath: Path = state.zlsPath.let { zlsPath -> - if (zlsPath.isEmpty()) { - val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv - env.findExecutableOnPATH("zls") ?: run { - Notification( - "zigbrains-lsp", - ZLSBundle.message("notification.message.could-not-detect.content"), - NotificationType.ERROR - ).notify(project) - return null - } - } else { - zlsPath.toNioPathOrNull() ?: run { - Notification( - "zigbrains-lsp", - ZLSBundle.message("notification.message.zls-exe-path-invalid.content", zlsPath), - NotificationType.ERROR - ).notify(project) - return null - } - } - } + val svc = ZLSService.getInstance(project) + val zls = svc.zls ?: return null + val zlsPath: Path = zls.path if (!zlsPath.toFile().exists()) { Notification( "zigbrains-lsp", @@ -95,7 +71,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje ).notify(project) return null } - val configPath: Path? = state.zlsConfigPath.let { configPath -> + val configPath: Path? = "".let { configPath -> if (configPath.isNotBlank()) { configPath.toNioPathOrNull()?.let { nioPath -> if (!nioPath.toFile().exists()) { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt index c46e9ac7..783f611f 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.lsp.settings.zlsSettings +import com.falsepattern.zigbrains.lsp.zls.ZLSService import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -68,39 +68,29 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS } features.inlayHintFeature = object: LSPInlayHintFeature() { override fun isEnabled(file: PsiFile): Boolean { - return features.project.zlsSettings.state.inlayHints + return ZLSService.getInstance(project).zls?.settings?.inlayHints == true } } return features } - override fun isEnabled(project: Project) = project.zlsEnabledSync() + override fun isEnabled(project: Project) = project.zlsEnabled() override fun setEnabled(enabled: Boolean, project: Project) { project.zlsEnabled(enabled) } } -suspend fun Project.zlsEnabledAsync(): Boolean { - return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateAsync() -} - -fun Project.zlsEnabledSync(): Boolean { - return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateSync() +fun Project.zlsEnabled(): Boolean { + return (getUserData(ENABLED_KEY) != false) && ZLSService.getInstance(this).zls?.isValid() == true } fun Project.zlsEnabled(value: Boolean) { putUserData(ENABLED_KEY, value) } -suspend fun Project.zlsRunningAsync(): Boolean { - if (!zlsEnabledAsync()) - return false - return lsm.isRunning -} - -fun Project.zlsRunningSync(): Boolean { - if (!zlsEnabledSync()) +fun Project.zlsRunning(): Boolean { + if (!zlsEnabled()) return false return lsm.isRunning } @@ -135,7 +125,7 @@ private suspend fun doStart(project: Project, restart: Boolean) { project.lsm.stop("ZigBrains") delay(250) } - if (project.zlsSettings.validateAsync()) { + if (ZLSService.getInstance(project).zls?.isValid() == true) { delay(250) project.lsm.start("ZigBrains") } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt index a812b8b8..026e7c22 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt @@ -23,8 +23,8 @@ package com.falsepattern.zigbrains.lsp.notification import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.lsp.zlsRunningAsync +import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.zlsRunning import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.zig.ZigFileType import com.falsepattern.zigbrains.zon.ZonFileType @@ -49,10 +49,10 @@ class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware { else -> return null } val task = project.zigCoroutineScope.async { - if (project.zlsRunningAsync()) { + if (project.zlsRunning()) { return@async null } else { - return@async project.zlsSettings.validateAsync() + return@async ZLSService.getInstance(project).zls?.isValid() == true } } return Function { editor -> diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt deleted file mode 100644 index 819a0bc2..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp.settings - -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv -import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.startLSP -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.intellij.ide.IdeEventQueue -import com.intellij.openapi.components.* -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.platform.ide.progress.runWithModalProgressBlocking -import com.intellij.util.application -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import java.nio.file.Path -import kotlin.io.path.isExecutable -import kotlin.io.path.isRegularFile - -@Service(Service.Level.PROJECT) -@State( - name = "ZLSSettings", - storages = [Storage(value = "zigbrains.xml")] -) -class ZLSProjectSettingsService(val project: Project): PersistentStateComponent { - @Volatile - private var state = ZLSSettings() - @Volatile - private var dirty = true - @Volatile - private var valid = false - - private val mutex = Mutex() - override fun getState(): ZLSSettings { - return state.copy() - } - - fun setState(value: ZLSSettings) { - runBlocking { - mutex.withLock { - this@ZLSProjectSettingsService.state = value - dirty = true - } - } - startLSP(project, true) - } - - override fun loadState(state: ZLSSettings) { - setState(state) - } - - suspend fun validateAsync(): Boolean { - mutex.withLock { - if (dirty) { - val state = this.state - valid = doValidate(project, state) - dirty = false - } - return valid - } - } - - fun validateSync(): Boolean { - val isValid: Boolean? = runBlocking { - mutex.withLock { - if (dirty) - null - else - valid - } - } - if (isValid != null) { - return isValid - } - return if (useModalProgress()) { - runWithModalProgressBlocking(ModalTaskOwner.project(project), ZLSBundle.message("progress.title.validate")) { - validateAsync() - } - } else { - runBlocking { - validateAsync() - } - } - } -} - -private val prohibitClass: Class<*>? = runCatching { - Class.forName("com_intellij_ide_ProhibitAWTEvents".replace('_', '.')) -}.getOrNull() - -private val postProcessors: List<*>? = runCatching { - if (prohibitClass == null) - return@runCatching null - val postProcessorsField = IdeEventQueue::class.java.getDeclaredField("postProcessors") - postProcessorsField.isAccessible = true - postProcessorsField.get(IdeEventQueue.getInstance()) as? List<*> -}.getOrNull() - -private fun useModalProgress(): Boolean { - if (!application.isDispatchThread) - return false - - if (application.isWriteAccessAllowed) - return false - - if (postProcessors == null) - return true - - return postProcessors.none { prohibitClass!!.isInstance(it) } -} - -private suspend fun doValidate(project: Project, state: ZLSSettings): Boolean { - val zlsPath: Path = state.zlsPath.let { zlsPath -> - if (zlsPath.isEmpty()) { - val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv - env.findExecutableOnPATH("zls") ?: run { - return false - } - } else { - zlsPath.toNioPathOrNull() ?: run { - return false - } - } - } - if (!zlsPath.toFile().exists()) { - return false - } - if (!zlsPath.isRegularFile() || !zlsPath.isExecutable()) { - return false - } - return true -} - -val Project.zlsSettings get() = service() \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt index d7f2f26e..75f29d74 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt @@ -25,35 +25,31 @@ package com.falsepattern.zigbrains.lsp.settings import com.falsepattern.zigbrains.lsp.config.SemanticTokens import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute import org.jetbrains.annotations.NonNls @Suppress("PropertyName") data class ZLSSettings( - var zlsPath: @NonNls String = "", - var zlsConfigPath: @NonNls String = "", - val inlayHints: Boolean = true, - val enable_snippets: Boolean = true, - val enable_argument_placeholders: Boolean = true, - val completion_label_details: Boolean = true, - val enable_build_on_save: Boolean = false, - val build_on_save_args: String = "", - val semantic_tokens: SemanticTokens = SemanticTokens.full, - val inlay_hints_show_variable_type_hints: Boolean = true, - val inlay_hints_show_struct_literal_field_type: Boolean = true, - val inlay_hints_show_parameter_name: Boolean = true, - val inlay_hints_show_builtin: Boolean = true, - val inlay_hints_exclude_single_argument: Boolean = true, - val inlay_hints_hide_redundant_param_names: Boolean = false, - val inlay_hints_hide_redundant_param_names_last_token: Boolean = false, - val warn_style: Boolean = false, - val highlight_global_var_declarations: Boolean = false, - val skip_std_references: Boolean = false, - val prefer_ast_check_as_child_process: Boolean = true, - val builtin_path: String? = null, - val build_runner_path: @NonNls String? = null, - val global_cache_path: @NonNls String? = null, -): ZigProjectConfigurationProvider.Settings { - override fun apply(project: Project) { - project.zlsSettings.loadState(this) - } -} + @JvmField @Attribute val zlsConfigPath: @NonNls String = "", + @JvmField @Attribute val inlayHints: Boolean = true, + @JvmField @Attribute val enable_snippets: Boolean = true, + @JvmField @Attribute val enable_argument_placeholders: Boolean = true, + @JvmField @Attribute val completion_label_details: Boolean = true, + @JvmField @Attribute val enable_build_on_save: Boolean = false, + @JvmField @Attribute val build_on_save_args: String = "", + @JvmField @Attribute val semantic_tokens: SemanticTokens = SemanticTokens.full, + @JvmField @Attribute val inlay_hints_show_variable_type_hints: Boolean = true, + @JvmField @Attribute val inlay_hints_show_struct_literal_field_type: Boolean = true, + @JvmField @Attribute val inlay_hints_show_parameter_name: Boolean = true, + @JvmField @Attribute val inlay_hints_show_builtin: Boolean = true, + @JvmField @Attribute val inlay_hints_exclude_single_argument: Boolean = true, + @JvmField @Attribute val inlay_hints_hide_redundant_param_names: Boolean = false, + @JvmField @Attribute val inlay_hints_hide_redundant_param_names_last_token: Boolean = false, + @JvmField @Attribute val warn_style: Boolean = false, + @JvmField @Attribute val highlight_global_var_declarations: Boolean = false, + @JvmField @Attribute val skip_std_references: Boolean = false, + @JvmField @Attribute val prefer_ast_check_as_child_process: Boolean = true, + @JvmField @Attribute val builtin_path: String? = null, + @JvmField @Attribute val build_runner_path: @NonNls String? = null, + @JvmField @Attribute val global_cache_path: @NonNls String? = null, +) \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt index b11e3ab4..43454bf7 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt @@ -24,12 +24,13 @@ package com.falsepattern.zigbrains.lsp.settings import com.falsepattern.zigbrains.lsp.config.ZLSConfig import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider +import com.falsepattern.zigbrains.lsp.zls.ZLSService import com.falsepattern.zigbrains.shared.cli.translateCommandline import com.intellij.openapi.project.Project class ZLSSettingsConfigProvider: ZLSConfigProvider { override fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { - val state = project.zlsSettings.state + val state = ZLSService.getInstance(project).zls?.settings ?: return previous return previous.copy( enable_snippets = state.enable_snippets, enable_argument_placeholders = state.enable_argument_placeholders, diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt deleted file mode 100644 index fad06b27..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp.settings - -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.Panel - -class ZLSSettingsConfigurable(private val project: Project): SubConfigurable { - private var appSettingsComponent: ZLSSettingsPanel? = null - override fun createComponent(holder: ZigProjectConfigurationProvider.SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel { - val settingsPanel = ZLSSettingsPanel(project).apply { attach(panel) }.also { Disposer.register(this, it) } - appSettingsComponent = settingsPanel - return settingsPanel - } - - override fun isModified(): Boolean { - val data = appSettingsComponent?.data ?: return false - return project.zlsSettings.state != data - } - - override fun apply() { - val data = appSettingsComponent?.data ?: return - val settings = project.zlsSettings - settings.state = data - } - - override fun reset() { - appSettingsComponent?.data = project.zlsSettings.state - } - - override fun dispose() { - appSettingsComponent = null - } -} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt deleted file mode 100644 index 9d28c16c..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt +++ /dev/null @@ -1,354 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp.settings - -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.direnv.Env -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv -import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.config.SemanticTokens -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.shared.cli.call -import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.execution.processTools.mapFlat -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.guessProjectDir -import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.openapi.vfs.toNioPathOrNull -import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.platform.ide.progress.TaskCancellation -import com.intellij.platform.ide.progress.withModalProgress -import com.intellij.ui.DocumentAdapter -import com.intellij.ui.JBColor -import com.intellij.ui.components.JBCheckBox -import com.intellij.ui.components.JBTextArea -import com.intellij.ui.components.fields.ExtendableTextField -import com.intellij.ui.components.textFieldWithBrowseButton -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.Row -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.jetbrains.annotations.PropertyKey -import javax.swing.event.DocumentEvent -import kotlin.io.path.pathString - -@Suppress("PrivatePropertyName") -class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel { - private val zlsPath = textFieldWithBrowseButton( - project, - FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() - .withTitle(ZLSBundle.message("settings.zls-path.browse.title")), - ).also { - it.textField.document.addDocumentListener(object: DocumentAdapter() { - override fun textChanged(p0: DocumentEvent) { - dispatchUpdateUI() - } - }) - Disposer.register(this, it) - } - private val zlsConfigPath = textFieldWithBrowseButton( - project, - FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() - .withTitle(ZLSBundle.message("settings.zls-config-path.browse.title")) - ).also { Disposer.register(this, it) } - - private val zlsVersion = JBTextArea().also { it.isEditable = false } - - private var debounce: Job? = null - - private var direnv: Boolean = project.zigProjectSettings.state.direnv - - private val inlayHints = JBCheckBox() - private val enable_snippets = JBCheckBox() - private val enable_argument_placeholders = JBCheckBox() - private val completion_label_details = JBCheckBox() - private val enable_build_on_save = JBCheckBox() - private val build_on_save_args = ExtendableTextField() - private val semantic_tokens = ComboBox(SemanticTokens.entries.toTypedArray()) - private val inlay_hints_show_variable_type_hints = JBCheckBox() - private val inlay_hints_show_struct_literal_field_type = JBCheckBox() - private val inlay_hints_show_parameter_name = JBCheckBox() - private val inlay_hints_show_builtin = JBCheckBox() - private val inlay_hints_exclude_single_argument = JBCheckBox() - private val inlay_hints_hide_redundant_param_names = JBCheckBox() - private val inlay_hints_hide_redundant_param_names_last_token = JBCheckBox() - private val warn_style = JBCheckBox() - private val highlight_global_var_declarations = JBCheckBox() - private val skip_std_references = JBCheckBox() - private val prefer_ast_check_as_child_process = JBCheckBox() - private val builtin_path = ExtendableTextField() - private val build_runner_path = ExtendableTextField() - private val global_cache_path = ExtendableTextField() - - override fun attach(p: Panel) = with(p) { - if (!project.isDefault) { - group(ZLSBundle.message("settings.group.title")) { - fancyRow( - "settings.zls-path.label", - "settings.zls-path.tooltip" - ) { - cell(zlsPath).resizableColumn().align(AlignX.FILL) - } - row(ZLSBundle.message("settings.zls-version.label")) { - cell(zlsVersion) - } - fancyRow( - "settings.zls-config-path.label", - "settings.zls-config-path.tooltip" - ) { cell(zlsConfigPath).align(AlignX.FILL) } - fancyRow( - "settings.enable_snippets.label", - "settings.enable_snippets.tooltip" - ) { cell(enable_snippets) } - fancyRow( - "settings.enable_argument_placeholders.label", - "settings.enable_argument_placeholders.tooltip" - ) { cell(enable_argument_placeholders) } - fancyRow( - "settings.completion_label_details.label", - "settings.completion_label_details.tooltip" - ) { cell(completion_label_details) } - fancyRow( - "settings.enable_build_on_save.label", - "settings.enable_build_on_save.tooltip" - ) { cell(enable_build_on_save) } - fancyRow( - "settings.build_on_save_args.label", - "settings.build_on_save_args.tooltip" - ) { cell(build_on_save_args).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.semantic_tokens.label", - "settings.semantic_tokens.tooltip" - ) { cell(semantic_tokens) } - group(ZLSBundle.message("settings.inlay-hints-group.label")) { - fancyRow( - "settings.inlay-hints-enable.label", - "settings.inlay-hints-enable.tooltip" - ) { cell(inlayHints) } - fancyRow( - "settings.inlay_hints_show_variable_type_hints.label", - "settings.inlay_hints_show_variable_type_hints.tooltip" - ) { cell(inlay_hints_show_variable_type_hints) } - fancyRow( - "settings.inlay_hints_show_struct_literal_field_type.label", - "settings.inlay_hints_show_struct_literal_field_type.tooltip" - ) { cell(inlay_hints_show_struct_literal_field_type) } - fancyRow( - "settings.inlay_hints_show_parameter_name.label", - "settings.inlay_hints_show_parameter_name.tooltip" - ) { cell(inlay_hints_show_parameter_name) } - fancyRow( - "settings.inlay_hints_show_builtin.label", - "settings.inlay_hints_show_builtin.tooltip" - ) { cell(inlay_hints_show_builtin) } - fancyRow( - "settings.inlay_hints_exclude_single_argument.label", - "settings.inlay_hints_exclude_single_argument.tooltip" - ) { cell(inlay_hints_exclude_single_argument) } - fancyRow( - "settings.inlay_hints_hide_redundant_param_names.label", - "settings.inlay_hints_hide_redundant_param_names.tooltip" - ) { cell(inlay_hints_hide_redundant_param_names) } - fancyRow( - "settings.inlay_hints_hide_redundant_param_names_last_token.label", - "settings.inlay_hints_hide_redundant_param_names_last_token.tooltip" - ) { cell(inlay_hints_hide_redundant_param_names_last_token) } - } - fancyRow( - "settings.warn_style.label", - "settings.warn_style.tooltip" - ) { cell(warn_style) } - fancyRow( - "settings.highlight_global_var_declarations.label", - "settings.highlight_global_var_declarations.tooltip" - ) { cell(highlight_global_var_declarations) } - fancyRow( - "settings.skip_std_references.label", - "settings.skip_std_references.tooltip" - ) { cell(skip_std_references) } - fancyRow( - "settings.prefer_ast_check_as_child_process.label", - "settings.prefer_ast_check_as_child_process.tooltip" - ) { cell(prefer_ast_check_as_child_process) } - fancyRow( - "settings.builtin_path.label", - "settings.builtin_path.tooltip" - ) { cell(builtin_path).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.build_runner_path.label", - "settings.build_runner_path.tooltip" - ) { cell(build_runner_path).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.global_cache_path.label", - "settings.global_cache_path.tooltip" - ) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) } - } - } - dispatchAutodetect(false) - } - - override fun direnvChanged(state: Boolean) { - direnv = state - dispatchAutodetect(true) - } - - override var data - get() = if (project.isDefault) ZLSSettings() else ZLSSettings( - zlsPath.text, - zlsConfigPath.text, - inlayHints.isSelected, - enable_snippets.isSelected, - enable_argument_placeholders.isSelected, - completion_label_details.isSelected, - enable_build_on_save.isSelected, - build_on_save_args.text, - semantic_tokens.item ?: SemanticTokens.full, - inlay_hints_show_variable_type_hints.isSelected, - inlay_hints_show_struct_literal_field_type.isSelected, - inlay_hints_show_parameter_name.isSelected, - inlay_hints_show_builtin.isSelected, - inlay_hints_exclude_single_argument.isSelected, - inlay_hints_hide_redundant_param_names.isSelected, - inlay_hints_hide_redundant_param_names_last_token.isSelected, - warn_style.isSelected, - highlight_global_var_declarations.isSelected, - skip_std_references.isSelected, - prefer_ast_check_as_child_process.isSelected, - builtin_path.text?.ifBlank { null }, - build_runner_path.text?.ifBlank { null }, - global_cache_path.text?.ifBlank { null }, - ) - set(value) { - zlsPath.text = value.zlsPath - zlsConfigPath.text = value.zlsConfigPath - inlayHints.isSelected = value.inlayHints - enable_snippets.isSelected = value.enable_snippets - enable_argument_placeholders.isSelected = value.enable_argument_placeholders - completion_label_details.isSelected = value.completion_label_details - enable_build_on_save.isSelected = value.enable_build_on_save - build_on_save_args.text = value.build_on_save_args - semantic_tokens.item = value.semantic_tokens - inlay_hints_show_variable_type_hints.isSelected = value.inlay_hints_show_variable_type_hints - inlay_hints_show_struct_literal_field_type.isSelected = value.inlay_hints_show_struct_literal_field_type - inlay_hints_show_parameter_name.isSelected = value.inlay_hints_show_parameter_name - inlay_hints_show_builtin.isSelected = value.inlay_hints_show_builtin - inlay_hints_exclude_single_argument.isSelected = value.inlay_hints_exclude_single_argument - inlay_hints_hide_redundant_param_names.isSelected = value.inlay_hints_hide_redundant_param_names - inlay_hints_hide_redundant_param_names_last_token.isSelected = - value.inlay_hints_hide_redundant_param_names_last_token - warn_style.isSelected = value.warn_style - highlight_global_var_declarations.isSelected = value.highlight_global_var_declarations - skip_std_references.isSelected = value.skip_std_references - prefer_ast_check_as_child_process.isSelected = value.prefer_ast_check_as_child_process - builtin_path.text = value.builtin_path ?: "" - build_runner_path.text = value.build_runner_path ?: "" - global_cache_path.text = value.global_cache_path ?: "" - dispatchUpdateUI() - } - - private fun dispatchAutodetect(force: Boolean) { - project.zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) { - withModalProgress(ModalTaskOwner.component(zlsPath), "Detecting ZLS...", TaskCancellation.cancellable()) { - autodetect(force) - } - } - } - - suspend fun autodetect(force: Boolean) { - if (force || zlsPath.text.isBlank()) { - getDirenv().findExecutableOnPATH("zls")?.let { - if (force || zlsPath.text.isBlank()) { - zlsPath.text = it.pathString - dispatchUpdateUI() - } - } - } - } - - override fun dispose() { - debounce?.cancel("Disposed") - } - - private suspend fun getDirenv(): Env { - if (!project.isDefault && DirenvCmd.direnvInstalled() && direnv) - return project.getDirenv() - return emptyEnv - } - - private fun dispatchUpdateUI() { - debounce?.cancel("New debounce") - debounce = project.zigCoroutineScope.launch { - updateUI() - } - } - - private suspend fun updateUI() { - if (project.isDefault) - return - delay(200) - val zlsPath = this.zlsPath.text.ifBlank { null }?.toNioPathOrNull() - if (zlsPath == null) { - withEDTContext(ModalityState.any()) { - zlsVersion.text = "[zls path empty or invalid]" - } - return - } - val workingDir = project.guessProjectDir()?.toNioPathOrNull() - val result = createCommandLineSafe(workingDir, zlsPath, "version") - .map { it.withEnvironment(getDirenv().env) } - .mapFlat { it.call() } - .getOrElse { throwable -> - throwable.printStackTrace() - withEDTContext(ModalityState.any()) { - zlsVersion.text = "[failed to run \"zls version\"]\n${throwable.message}" - } - return - } - val version = result.stdout.trim() - withEDTContext(ModalityState.any()) { - zlsVersion.text = version - zlsVersion.foreground = JBColor.foreground() - } - } -} - -private fun Panel.fancyRow( - label: @PropertyKey(resourceBundle = "zigbrains.lsp.Bundle") String, - tooltip: @PropertyKey(resourceBundle = "zigbrains.lsp.Bundle") String, - cb: Row.() -> Unit -) = row(ZLSBundle.message(label)) { - contextHelp(ZLSBundle.message(tooltip)) - cb() -} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt new file mode 100644 index 00000000..b38a3287 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt @@ -0,0 +1,85 @@ +/* + * 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.lsp.zls + +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.NlsContexts +import com.intellij.openapi.util.NlsSafe +import com.intellij.ui.dsl.builder.panel +import java.util.UUID +import javax.swing.JComponent + +class ZLSConfigurable(val uuid: UUID, zls: ZLSVersion): NamedConfigurable() { + var zls: ZLSVersion = zls + set(value) { + zlsInstallations[uuid] = value + field = value + } + private var myView: ZLSPanel? = null + + override fun setDisplayName(name: String?) { + zls = zls.copy(name = name) + } + + override fun getEditableObject(): UUID? { + return uuid + } + + override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { + return displayName + } + + override fun createOptionsPanel(): JComponent? { + var view = myView + if (view == null) { + view = ZLSPanel() + view.reset(zls) + myView = view + } + return panel { + view.attach(this@panel) + }.withMaximumWidth(20) + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return zls.name + } + + override fun isModified(): Boolean { + return myView?.isModified(zls) == true + } + + override fun apply() { + myView?.apply(zls)?.let { zls = it } + } + + override fun reset() { + myView?.reset(zls) + } + + override fun disposeUIResources() { + myView?.dispose() + myView = null + super.disposeUIResources() + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt new file mode 100644 index 00000000..70a13e52 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt @@ -0,0 +1,51 @@ +/* + * 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.lsp.zls + +import com.falsepattern.zigbrains.lsp.zls.ZLSInstallationsService.MyState +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.falsepattern.zigbrains.shared.UUIDStorage +import com.intellij.openapi.components.* + +@Service(Service.Level.APP) +@State( + name = "ZLSInstallations", + storages = [Storage("zigbrains.xml")] +) +class ZLSInstallationsService: UUIDMapSerializable.Converting(MyState()) { + override fun serialize(value: ZLSVersion) = value.toRef() + override fun deserialize(value: ZLSVersion.Ref) = value.resolve() + override fun getStorage(state: MyState) = state.zlsInstallations + override fun updateStorage(state: MyState, storage: ZLSStorage) = state.copy(zlsInstallations = storage) + + data class MyState(@JvmField val zlsInstallations: ZLSStorage = emptyMap()) + + companion object { + @JvmStatic + fun getInstance(): ZLSInstallationsService = service() + } +} + +inline val zlsInstallations: ZLSInstallationsService get() = ZLSInstallationsService.getInstance() + +private typealias ZLSStorage = UUIDStorage \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt new file mode 100644 index 00000000..2f93225b --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt @@ -0,0 +1,131 @@ +/* + * 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.lsp.zls + +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase +import com.falsepattern.zigbrains.shared.cli.call +import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString + +class ZLSPanel() : ImmutableNamedElementPanelBase() { + private val pathToZLS = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("Path to the zls executable") + ).also { + it.textField.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + dispatchUpdateUI() + } + }) + Disposer.register(this, it) + } + private val zlsVersion = JBTextArea().also { it.isEditable = false } + private var debounce: Job? = null + + override fun attach(p: Panel): Unit = with(p) { + super.attach(p) + row("Path:") { + cell(pathToZLS).resizableColumn().align(AlignX.FILL) + } + row("Version:") { + cell(zlsVersion) + } + } + + override fun isModified(version: ZLSVersion): Boolean { + val name = nameFieldValue ?: return false + val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return false + return name != version.name || version.path != path + } + + override fun apply(version: ZLSVersion): ZLSVersion? { + val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return null + return version.copy(path = path, name = nameFieldValue ?: "") + } + + override fun reset(version: ZLSVersion) { + nameFieldValue = version.name + this.pathToZLS.text = version.path.pathString + dispatchUpdateUI() + } + + private fun dispatchUpdateUI() { + debounce?.cancel("New debounce") + debounce = zigCoroutineScope.launch { + updateUI() + } + } + + private suspend fun updateUI() { + delay(200) + val pathToZLS = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() + if (pathToZLS == null) { + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[zls path empty or invalid]" + } + return + } + val versionCommand = createCommandLineSafe(null, pathToZLS, "--version").getOrElse { + it.printStackTrace() + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[could not create \"zls --version\" command]\n${it.message}" + } + return + } + val result = versionCommand.call().getOrElse { + it.printStackTrace() + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[failed to run \"zls --version\"]\n${it.message}" + } + return + } + val version = result.stdout.trim() + + withEDTContext(ModalityState.any()) { + zlsVersion.text = version + zlsVersion.foreground = JBColor.foreground() + } + } + + override fun dispose() { + debounce?.cancel("Disposed") + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt new file mode 100644 index 00000000..500e92f4 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt @@ -0,0 +1,63 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.intellij.openapi.components.* +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute +import java.util.UUID + +@Service(Service.Level.PROJECT) +@State( + name = "ZLS", + storages = [Storage("zigbrains.xml")] +) +class ZLSService: SerializablePersistentStateComponent(MyState()) { + var zlsUUID: UUID? + get() = state.zls.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { + if (it in zigToolchainList) { + true + } else { + updateState { + it.copy(zls = "") + } + false + } + } + set(value) { + updateState { + it.copy(zls = value?.toString() ?: "") + } + } + + val zls: ZLSVersion? + get() = zlsUUID?.let { zlsInstallations[it] } + + data class MyState(@JvmField @Attribute var zls: String = "") + + companion object { + @JvmStatic + fun getInstance(project: Project): ZLSService = project.service() + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt new file mode 100644 index 00000000..2778c77a --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt @@ -0,0 +1,65 @@ +/* + * 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.lsp.zls + +import com.falsepattern.zigbrains.lsp.settings.ZLSSettings +import com.falsepattern.zigbrains.shared.NamedObject +import com.intellij.openapi.util.io.toNioPathOrNull +import java.nio.file.Path +import com.intellij.util.xmlb.annotations.Attribute +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile +import kotlin.io.path.pathString + +data class ZLSVersion(val path: Path, override val name: String?, val settings: ZLSSettings): NamedObject { + override fun withName(newName: String?): ZLSVersion { + return copy(name = newName) + } + + fun toRef(): Ref { + return Ref(path.pathString, name, settings) + } + + fun isValid(): Boolean { + if (!path.toFile().exists()) + return false + if (!path.isRegularFile() || !path.isExecutable()) + return false + return true + } + + data class Ref( + @JvmField + @Attribute + val path: String? = "", + @JvmField + @Attribute + val name: String? = "", + @JvmField + val settings: ZLSSettings = ZLSSettings() + ) { + fun resolve(): ZLSVersion? { + return path?.ifBlank { null }?.toNioPathOrNull()?.let { ZLSVersion(it, name, settings) } + } + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt new file mode 100644 index 00000000..ce6d5ec8 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt @@ -0,0 +1,67 @@ +/* + * 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.lsp.zls.ui + +import com.falsepattern.zigbrains.lsp.zls.ZLSConfigurable +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.lsp.zls.zlsInstallations +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ListElemIn +import com.falsepattern.zigbrains.shared.ui.UUIDComboBoxDriver +import com.falsepattern.zigbrains.shared.ui.ZBComboBox +import com.falsepattern.zigbrains.shared.ui.ZBContext +import com.falsepattern.zigbrains.shared.ui.ZBModel +import com.intellij.openapi.ui.NamedConfigurable +import java.awt.Component +import java.util.UUID + +object ZLSDriver: UUIDComboBoxDriver { + override val theMap: UUIDMapSerializable.Converting + get() = zlsInstallations + + override fun constructModelList(): List> { + return ListElem.fetchGroup() + } + + override fun createContext(model: ZBModel): ZBContext { + return ZLSContext(null, model) + } + + override fun createComboBox(model: ZBModel): ZBComboBox { + return ZLSComboBox(model) + } + + override suspend fun resolvePseudo( + context: Component, + elem: ListElem.Pseudo + ): UUID? { + //TODO + return null + } + + override fun createNamedConfigurable(uuid: UUID, elem: ZLSVersion): NamedConfigurable { + //TODO + return ZLSConfigurable(uuid, elem) + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt new file mode 100644 index 00000000..f93d1b00 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt @@ -0,0 +1,89 @@ +/* + * 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.lsp.zls.ui + +import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.shared.SubConfigurable +import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.util.Key +import com.intellij.ui.dsl.builder.Panel +import kotlinx.coroutines.launch + +class ZLSEditor(private var project: Project?, + private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): + UUIDMapSelector(ZLSDriver), + SubConfigurable, + ZigProjectConfigurationProvider.UserDataListener +{ + init { + sharedState.addUserDataChangeListener(this) + } + + override fun onUserDataChanged(key: Key<*>) { + zigCoroutineScope.launch { listChanged() } + } + + override fun attach(panel: Panel): Unit = with(panel) { + row("ZLS") { + attachComboBoxRow(this) + } + } + + override fun isModified(context: Project): Boolean { + return ZLSService.getInstance(context).zlsUUID != selectedUUID + } + + override fun apply(context: Project) { + ZLSService.getInstance(context).zlsUUID = selectedUUID + } + + override fun reset(context: Project?) { + val project = context ?: ProjectManager.getInstance().defaultProject + selectedUUID = ZLSService.getInstance(project).zlsUUID + } + + override fun dispose() { + super.dispose() + sharedState.removeUserDataChangeListener(this) + } + + override val newProjectBeforeInitSelector: Boolean get() = true + class Provider: ZigProjectConfigurationProvider { + override fun create( + project: Project?, + sharedState: ZigProjectConfigurationProvider.IUserDataBridge + ): SubConfigurable? { + return ZLSEditor(project, sharedState) + } + + override val index: Int + get() = 50 + + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt new file mode 100644 index 00000000..3ee4e221 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt @@ -0,0 +1,39 @@ +/* + * 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.lsp.zls.ui + +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +import com.intellij.openapi.ui.MasterDetailsComponent +import com.intellij.openapi.util.NlsContexts + +class ZLSListEditor : UUIDMapEditor(ZLSDriver) { + override fun getEmptySelectionString(): String { + return ZLSBundle.message("settings.list.empty") + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return ZLSBundle.message("settings.list.title") + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt new file mode 100644 index 00000000..cde2f702 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt @@ -0,0 +1,92 @@ +/* + * 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.lsp.zls.ui + +import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.project.toolchain.base.render +import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer +import com.falsepattern.zigbrains.shared.ui.ZBComboBox +import com.falsepattern.zigbrains.shared.ui.ZBContext +import com.falsepattern.zigbrains.shared.ui.ZBModel +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.icons.EMPTY_ICON +import javax.swing.JList +import kotlin.io.path.pathString + + +class ZLSComboBox(model: ZBModel): ZBComboBox(model, ::ZLSCellRenderer) + +class ZLSContext(project: Project?, model: ZBModel): ZBContext(project, model, ::ZLSCellRenderer) + +class ZLSCellRenderer(getModel: () -> ZBModel): ZBCellRenderer(getModel) { + override fun customizeCellRenderer( + list: JList?>, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + icon = EMPTY_ICON + when (value) { + is ListElem.One -> { + val (icon, isSuggestion) = when(value) { + is ListElem.One.Suggested -> AllIcons.General.Information to true + is ListElem.One.Actual -> Icons.Zig to false + } + this.icon = icon + val item = value.instance + //TODO proper renderer + if (item.name != null) { + append(item.name) + append(item.path.pathString, SimpleTextAttributes.GRAYED_ATTRIBUTES) + } else { + append(item.path.pathString) + } + } + + is ListElem.Download -> { + icon = AllIcons.Actions.Download + append(ZLSBundle.message("settings.model.download.text")) + } + + is ListElem.FromDisk -> { + icon = AllIcons.General.OpenDisk + append(ZLSBundle.message("settings.model.from-disk.text")) + } + is ListElem.Pending -> { + icon = AllIcons.Empty + append(ZLSBundle.message("settings.model.loading.text"), SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + is ListElem.None, null -> { + icon = AllIcons.General.BalloonError + append(ZLSBundle.message("settings.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } + +} \ No newline at end of file diff --git a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml index d4b6c582..0182a606 100644 --- a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml +++ b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml @@ -49,6 +49,13 @@ + @@ -59,7 +66,7 @@ implementation="com.falsepattern.zigbrains.lsp.ToolchainZLSConfigProvider" /> diff --git a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties index 3b76263f..1696dace 100644 --- a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties +++ b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties @@ -66,3 +66,10 @@ progress.title.validate=Validating ZLS lsp.zls.name=Zig Language Server # suppress inspection "UnusedProperty" lsp.zls.description=The Zig Language Server, via ZigBrains +settings.list.title=ZLS Instances +settings.list.empty=Select a ZLS version to view or edit its details here +settings.model.detected.separator=Detected ZLS version +settings.model.none.text= +settings.model.loading.text=Loading\u2026 +settings.model.from-disk.text=Add ZLS from disk\u2026 +settings.model.download.text=Download ZLS\u2026 \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 78171fe4..1656a8e0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -17,6 +17,11 @@ dynamic="true" name="zlsConfigProvider" /> +