initial LSP work

This commit is contained in:
FalsePattern 2025-04-10 20:10:27 +02:00
parent 137977f691
commit dcede7eb43
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
49 changed files with 1611 additions and 1342 deletions

View file

@ -83,7 +83,7 @@ allprojects {
} }
} }
filter { filter {
// includeModule("com.redhat.devtools.intellij", "lsp4ij") includeModule("com.redhat.devtools.intellij", "lsp4ij")
} }
} }
mavenCentral() mavenCentral()
@ -104,12 +104,12 @@ dependencies {
pluginVerifier() pluginVerifier()
zipSigner() zipSigner()
// plugin(lsp4ijPluginString) plugin(lsp4ijPluginString)
} }
runtimeOnly(project(":core")) runtimeOnly(project(":core"))
runtimeOnly(project(":cidr")) runtimeOnly(project(":cidr"))
// runtimeOnly(project(":lsp")) runtimeOnly(project(":lsp"))
} }
intellijPlatform { intellijPlatform {

View file

@ -92,6 +92,6 @@ abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationP
} }
override val index: Int override val index: Int
get() = 1 get() = 100
} }
} }

View file

@ -35,7 +35,6 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.NlsActions.ActionText import com.intellij.openapi.util.NlsActions.ActionText
import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.util.progress.reportRawProgress
import org.jdom.Element import org.jdom.Element
import org.jetbrains.annotations.Nls import org.jetbrains.annotations.Nls

View file

@ -25,8 +25,6 @@ package com.falsepattern.zigbrains.project.settings
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.project.Project 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<Project>() { class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter<Project>() {
override fun instantiate(): List<SubConfigurable<Project>> { override fun instantiate(): List<SubConfigurable<Project>> {

View file

@ -42,8 +42,8 @@ import java.util.UUID
name = "ZigToolchain", name = "ZigToolchain",
storages = [Storage("zigbrains.xml")] storages = [Storage("zigbrains.xml")]
) )
class ZigToolchainService(val project: Project): SerializablePersistentStateComponent<ZigToolchainService.State>(State()), IZigToolchainService { class ZigToolchainService(private val project: Project): SerializablePersistentStateComponent<ZigToolchainService.State>(State()) {
override var toolchainUUID: UUID? var toolchainUUID: UUID?
get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf {
if (it in zigToolchainList) { if (it in zigToolchainList) {
true true
@ -63,7 +63,7 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp
} }
} }
override val toolchain: ZigToolchain? val toolchain: ZigToolchain?
get() = toolchainUUID?.let { zigToolchainList[it] } get() = toolchainUUID?.let { zigToolchainList[it] }
data class State( data class State(
@ -74,11 +74,6 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp
companion object { companion object {
@JvmStatic @JvmStatic
fun getInstance(project: Project): IZigToolchainService = project.service<ZigToolchainService>() fun getInstance(project: Project): ZigToolchainService = project.service<ZigToolchainService>()
} }
}
sealed interface IZigToolchainService {
var toolchainUUID: UUID?
val toolchain: ZigToolchain?
} }

View file

@ -22,6 +22,7 @@
package com.falsepattern.zigbrains.project.toolchain.base package com.falsepattern.zigbrains.project.toolchain.base
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
@ -38,14 +39,14 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
zigToolchainList[uuid] = value zigToolchainList[uuid] = value
field = value field = value
} }
private var myViews: List<ZigToolchainPanel<T>> = emptyList() private var myViews: List<ImmutableElementPanel<T>> = emptyList()
abstract fun createPanel(): ZigToolchainPanel<T> abstract fun createPanel(): ImmutableElementPanel<T>
override fun createOptionsPanel(): JComponent? { override fun createOptionsPanel(): JComponent? {
var views = myViews var views = myViews
if (views.isEmpty()) { if (views.isEmpty()) {
views = ArrayList<ZigToolchainPanel<T>>() views = ArrayList<ImmutableElementPanel<T>>()
views.add(createPanel()) views.add(createPanel())
views.addAll(createZigToolchainExtensionPanels()) views.addAll(createZigToolchainExtensionPanels())
views.forEach { it.reset(toolchain) } views.forEach { it.reset(toolchain) }

View file

@ -22,16 +22,17 @@
package com.falsepattern.zigbrains.project.toolchain.base package com.falsepattern.zigbrains.project.toolchain.base
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.extensions.ExtensionPointName
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainExtensionsProvider>("com.falsepattern.zigbrains.toolchainExtensionsProvider") private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainExtensionsProvider>("com.falsepattern.zigbrains.toolchainExtensionsProvider")
internal interface ZigToolchainExtensionsProvider { internal interface ZigToolchainExtensionsProvider {
fun <T : ZigToolchain> createExtensionPanel(): ZigToolchainPanel<T>? fun <T : ZigToolchain> createExtensionPanel(): ImmutableElementPanel<T>?
val index: Int val index: Int
} }
fun <T: ZigToolchain> createZigToolchainExtensionPanels(): List<ZigToolchainPanel<T>> { fun <T: ZigToolchain> createZigToolchainExtensionPanels(): List<ImmutableElementPanel<T>> {
return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull { return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull {
it.createExtensionPanel() it.createExtensionPanel()
} }

View file

@ -49,7 +49,6 @@ import com.intellij.util.asSafely
import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.concurrency.annotations.RequiresEdt
import java.awt.Component import java.awt.Component
import java.nio.file.Path import java.nio.file.Path
import java.util.*
import javax.swing.DefaultComboBoxModel import javax.swing.DefaultComboBoxModel
import javax.swing.JList import javax.swing.JList
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent

View file

@ -50,10 +50,8 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import java.io.File import java.io.File
import java.lang.IllegalStateException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.*
import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively import kotlin.io.path.deleteRecursively
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory

View file

@ -23,7 +23,7 @@
package com.falsepattern.zigbrains.project.toolchain.local package com.falsepattern.zigbrains.project.toolchain.local
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainPanelBase import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ModalityState
@ -44,7 +44,7 @@ import kotlinx.coroutines.launch
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString import kotlin.io.path.pathString
class LocalZigToolchainPanel() : ZigToolchainPanelBase<LocalZigToolchain>() { class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase<LocalZigToolchain>() {
private val pathToToolchain = textFieldWithBrowseButton( private val pathToToolchain = textFieldWithBrowseButton(
null, null,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))

View file

@ -23,7 +23,6 @@
package com.falsepattern.zigbrains.project.toolchain.local package com.falsepattern.zigbrains.project.toolchain.local
import com.falsepattern.zigbrains.direnv.DirenvService import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.direnv.DirenvState
import com.falsepattern.zigbrains.direnv.Env import com.falsepattern.zigbrains.direnv.Env
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable

View file

@ -20,17 +20,17 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.toolchain.base package com.falsepattern.zigbrains.project.toolchain.ui
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Panel
interface ZigToolchainPanel<T: ZigToolchain>: Disposable { interface ImmutableElementPanel<T>: Disposable {
fun attach(p: Panel) 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. * Returned object must be the exact same class as the provided one.
*/ */
fun apply(toolchain: T): T? fun apply(elem: T): T?
fun reset(toolchain: T) fun reset(elem: T)
} }

View file

@ -20,17 +20,15 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.toolchain.base package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle 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.components.JBTextField
import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.util.preferredHeight
import java.awt.Dimension
abstract class ZigToolchainPanelBase<T: ZigToolchain>: ZigToolchainPanel<T> { abstract class ImmutableNamedElementPanelBase<T>: ImmutableElementPanel<T> {
private val nameField = JBTextField(25) private val nameField = JBTextField(25)
protected var nameFieldValue: String? protected var nameFieldValue: String?

View file

@ -22,10 +22,11 @@
package com.falsepattern.zigbrains.project.toolchain.ui 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.Downloader
import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.withUniqueName import com.falsepattern.zigbrains.shared.withUniqueName
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import java.awt.Component import java.awt.Component
@ -33,9 +34,9 @@ import java.util.UUID
internal object ZigToolchainComboBoxHandler { internal object ZigToolchainComboBoxHandler {
@RequiresBackgroundThread @RequiresBackgroundThread
suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) { suspend fun onItemSelected(context: Component, elem: ListElem.Pseudo<ZigToolchain>): UUID? = when(elem) {
is TCListElem.Toolchain.Suggested -> zigToolchainList.withUniqueName(elem.toolchain) is ListElem.One.Suggested -> zigToolchainList.withUniqueName(elem.instance)
is TCListElem.Download -> Downloader.downloadToolchain(context) is ListElem.Download -> Downloader.downloadToolchain(context)
is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context) is ListElem.FromDisk -> LocalSelector.browseFromDisk(context)
}?.let { zigToolchainList.registerNew(it) } }?.let { zigToolchainList.registerNew(it) }
} }

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZigToolchain> {
override val theMap get() = zigToolchainList
override fun createContext(model: ZBModel<ZigToolchain>): ZBContext<ZigToolchain> {
return TCContext(null, model)
}
override fun createComboBox(model: ZBModel<ZigToolchain>): ZBComboBox<ZigToolchain> {
return TCComboBox(model)
}
override suspend fun resolvePseudo(
context: Component,
elem: ListElem.Pseudo<ZigToolchain>
): UUID? {
return ZigToolchainComboBoxHandler.onItemSelected(context, elem)
}
override fun createNamedConfigurable(
uuid: UUID,
elem: ZigToolchain
): NamedConfigurable<UUID> {
return elem.createNamedConfigurable(uuid)
}
object ForList: ZigToolchainDriver {
override fun constructModelList(): List<ListElemIn<ZigToolchain>> {
val modelList = ArrayList<ListElemIn<ZigToolchain>>()
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<ListElemIn<ZigToolchain>> {
val modelList = ArrayList<ListElemIn<ZigToolchain>>()
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
}
}
}

View file

@ -24,113 +24,29 @@ package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider 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.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.StorageChangeListener
import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.SubConfigurable
import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope 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.Project
import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.Key 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.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.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<Project>, ZigProjectConfigurationProvider.UserDataListener { class ZigToolchainEditor(private var project: Project?,
private val toolchainBox: TCComboBox private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge):
private var selectOnNextReload: UUID? = null UUIDMapSelector<ZigToolchain>(ZigToolchainDriver.ForSelector(project, sharedState)),
private val model: TCModel SubConfigurable<Project>,
private var editButton: JButton? = null ZigProjectConfigurationProvider.UserDataListener
private val changeListener: StorageChangeListener = { this@ZigToolchainEditor.toolchainListChanged() } {
init { init {
model = TCModel(getModelList(project, sharedState))
toolchainBox = TCComboBox(model)
toolchainBox.addItemListener(::itemStateChanged)
zigToolchainList.addChangeListener(changeListener)
sharedState.addUserDataChangeListener(this) 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<*>) { 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 else
"settings.toolchain.editor.toolchain.label") "settings.toolchain.editor.toolchain.label")
) { ) {
cell(toolchainBox).resizableColumn().align(AlignX.FILL) attachComboBoxRow(this)
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
} }
} }
override fun isModified(context: Project): Boolean { override fun isModified(context: Project): Boolean {
return ZigToolchainService.getInstance(context).toolchainUUID != toolchainBox.selectedToolchain return ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID
} }
override fun apply(context: Project) { override fun apply(context: Project) {
ZigToolchainService.getInstance(context).toolchainUUID = toolchainBox.selectedToolchain ZigToolchainService.getInstance(context).toolchainUUID = selectedUUID
} }
override fun reset(context: Project?) { override fun reset(context: Project?) {
val project = context ?: ProjectManager.getInstance().defaultProject val project = context ?: ProjectManager.getInstance().defaultProject
toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID selectedUUID = ZigToolchainService.getInstance(project).toolchainUUID
refreshButtonState(toolchainBox.selectedItem)
} }
override fun dispose() { override fun dispose() {
zigToolchainList.removeChangeListener(changeListener) super.dispose()
sharedState.removeUserDataChangeListener(this)
} }
override val newProjectBeforeInitSelector get() = true override val newProjectBeforeInitSelector get() = true
@ -195,16 +87,4 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
override val index: Int get() = 0 override val index: Int get() = 0
} }
}
private fun getModelList(project: Project?, data: UserDataHolder): List<TCListElemIn> {
val modelList = ArrayList<TCListElemIn>()
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
} }

View file

@ -23,143 +23,9 @@
package com.falsepattern.zigbrains.project.toolchain.ui package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor
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<AnAction> {
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<TCListElemIn>()
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")
class ZigToolchainListEditor : UUIDMapEditor<ZigToolchain>(ZigToolchainDriver.ForList) {
override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title") override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title")
override fun disposeUIResources() {
super.disposeUIResources()
if (registered) {
zigToolchainList.removeChangeListener(changeListener)
}
}
private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) {
val node = MyNode(toolchain.createNamedConfigurable(uuid))
addNode(node, myRoot)
}
private fun reloadTree() {
val currentSelection = selectedObject?.asSafely<UUID>()
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<UUID>()
if (uuid != null && uuid != currentSelection) {
selectOnNextReload = uuid
} else {
selectOnNextReload = null
}
}
private suspend fun toolchainListChanged() {
withEDTContext(myWholePanel.asContextElement()) {
reloadTree()
}
}
} }

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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>): TCListElem
companion object {
val fetchGroup get() = listOf(Download, FromDisk)
}
}
@JvmRecord
internal data class Separator(val text: String, val line: Boolean) : TCListElemIn
internal fun Pair<UUID, ZigToolchain>.asActual() = TCListElem.Toolchain.Actual(first, second)
internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this)
internal fun Flow<ZigToolchain>.asPending() = TCListElem.Pending(map { it.asSuggested() })

View file

@ -24,269 +24,61 @@ package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.ZigBrainsBundle 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.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.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.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.SimpleTextAttributes
import com.intellij.ui.components.panels.OpaquePanel import com.intellij.ui.icons.EMPTY_ICON
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 javax.swing.JList import javax.swing.JList
import javax.swing.border.Border
internal class TCComboBoxPopup( class TCComboBox(model: ZBModel<ZigToolchain>): ZBComboBox<ZigToolchain>(model, ::TCCellRenderer)
context: TCContext,
selected: TCListElem?,
onItemSelected: Consumer<TCListElem>,
) : ComboBoxPopup<TCListElem>(context, selected, onItemSelected)
internal class TCComboBox(model: TCModel): ComboBox<TCListElem>(model) { class TCContext(project: Project?, model: ZBModel<ZigToolchain>): ZBContext<ZigToolchain>(project, model, ::TCCellRenderer)
init {
setRenderer(TCCellRenderer { model })
}
var selectedToolchain: UUID?
set(value) {
if (value == null) {
selectedItem = TCListElem.None
return
}
for (i in 0..<model.size) {
val element = model.getElementAt(i)
if (element is TCListElem.Toolchain.Actual) {
if (element.uuid == value) {
selectedIndex = i
return
}
}
}
selectedItem = TCListElem.None
}
get() {
val item = selectedItem
return when(item) {
is TCListElem.Toolchain.Actual -> item.uuid
else -> null
}
}
}
internal class TCModel private constructor(elements: List<TCListElem>, private var separators: MutableMap<TCListElem, Separator>) : CollectionComboBoxModel<TCListElem>(elements) {
private var counter: Int = 0
companion object {
operator fun invoke(input: List<TCListElemIn>): TCModel {
val (elements, separators) = convert(input)
val model = TCModel(elements, separators)
model.launchPendingResolve()
return model
}
private fun convert(input: List<TCListElemIn>): Pair<List<TCListElem>, MutableMap<TCListElem, Separator>> {
val separators = IdentityHashMap<TCListElem, Separator>()
var lastSeparator: Separator? = null
val elements = ArrayList<TCListElem>()
input.forEach {
when (it) {
is TCListElem -> {
if (lastSeparator != null) {
separators[it] = lastSeparator
lastSeparator = null
}
elements.add(it)
}
is Separator -> lastSeparator = it
}
}
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..<size) {
val elem = getElementAt(i)
?: continue
if (elem !is TCListElem.Pending)
continue
zigCoroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) {
elem.elems.collect { newElem ->
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<TCListElemIn>) {
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<TCListElem> {
override fun getProject(): Project? {
return project
}
override fun getModel(): TCModel {
return model
}
override fun getRenderer(): TCCellRenderer {
return TCCellRenderer(::getModel)
}
}
internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer<TCListElem>() {
override fun getListCellRendererComponent(
list: JList<out TCListElem?>?,
value: TCListElem?,
index: Int,
selected: Boolean,
hasFocus: Boolean
): Component? {
val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent
val panel = object : CellRendererPanel(BorderLayout()) {
val myContext = component.accessibleContext
override fun getAccessibleContext(): AccessibleContext? {
return myContext
}
override fun setBorder(border: Border?) {
component.border = border
}
}
panel.add(component, BorderLayout.CENTER)
component.isOpaque = true
list?.let { background = if (selected) it.selectionBackground else it.background }
val model = getModel()
if (index == -1) {
component.isOpaque = false
panel.isOpaque = false
return panel
}
val separator = value?.let { model.separatorAbove(it) }
if (separator != null) {
val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3
val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0))
separatorComponent.isHideLine = !separator.line
separatorComponent.caption = separator.text.ifBlank { null }
val wrapper = OpaquePanel(BorderLayout())
wrapper.add(separatorComponent, BorderLayout.CENTER)
list?.let { wrapper.background = it.background }
panel.add(wrapper, BorderLayout.NORTH)
}
return panel
}
class TCCellRenderer(getModel: () -> ZBModel<ZigToolchain>): ZBCellRenderer<ZigToolchain>(getModel) {
override fun customizeCellRenderer( override fun customizeCellRenderer(
list: JList<out TCListElem?>, list: JList<out ListElem<ZigToolchain>?>,
value: TCListElem?, value: ListElem<ZigToolchain>?,
index: Int, index: Int,
selected: Boolean, selected: Boolean,
hasFocus: Boolean hasFocus: Boolean
) { ) {
icon = EMPTY_ICON icon = EMPTY_ICON
when (value) { when (value) {
is TCListElem.Toolchain -> { is ListElem.One -> {
val (icon, isSuggestion) = when(value) { val (icon, isSuggestion) = when(value) {
is TCListElem.Toolchain.Suggested -> AllIcons.General.Information to true is ListElem.One.Suggested -> AllIcons.General.Information to true
is TCListElem.Toolchain.Actual -> Icons.Zig to false is ListElem.One.Actual -> Icons.Zig to false
} }
this.icon = icon this.icon = icon
val toolchain = value.toolchain val item = value.instance
toolchain.render(this, isSuggestion, index == -1) item.render(this, isSuggestion, index == -1)
} }
is TCListElem.Download -> { is ListElem.Download -> {
icon = AllIcons.Actions.Download icon = AllIcons.Actions.Download
append(ZigBrainsBundle.message("settings.toolchain.model.download.text")) append(ZigBrainsBundle.message("settings.toolchain.model.download.text"))
} }
is TCListElem.FromDisk -> { is ListElem.FromDisk -> {
icon = AllIcons.General.OpenDisk icon = AllIcons.General.OpenDisk
append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text")) append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text"))
} }
is TCListElem.Pending -> { is ListElem.Pending -> {
icon = AllIcons.Empty 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 icon = AllIcons.General.BalloonError
append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES)
} }
} }
} }
}
private val EMPTY_ICON = EmptyIcon.create(1, 16) }

View file

@ -28,7 +28,6 @@ import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ipc import com.falsepattern.zigbrains.shared.ipc.ipc
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.options.ConfigurationException

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T> {
val theMap: UUIDMapSerializable.Converting<T, *, *>
fun constructModelList(): List<ListElemIn<T>>
fun createContext(model: ZBModel<T>): ZBContext<T>
fun createComboBox(model: ZBModel<T>): ZBComboBox<T>
suspend fun resolvePseudo(context: Component, elem: ListElem.Pseudo<T>): UUID?
fun createNamedConfigurable(uuid: UUID, elem: T): NamedConfigurable<UUID>
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T>(val driver: UUIDComboBoxDriver<T>): 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<AnAction> {
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<T>) {
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<UUID>()
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<UUID>()
if (uuid != null && uuid != currentSelection) {
selectOnNextReload = uuid
} else {
selectOnNextReload = null
}
}
private suspend fun listChanged() {
withEDTContext(myWholePanel.asContextElement()) {
reloadTree()
}
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T>(val driver: UUIDComboBoxDriver<T>): Disposable {
private val comboBox: ZBComboBox<T>
private var selectOnNextReload: UUID? = null
private val model: ZBModel<T>
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<T>
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<Any>()
}
}
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)
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.shared.ui
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.UUID
sealed interface ListElemIn<T>
@Suppress("UNCHECKED_CAST")
sealed interface ListElem<T> : ListElemIn<T> {
sealed interface Pseudo<T>: ListElem<T>
sealed interface One<T> : ListElem<T> {
val instance: T
@JvmRecord
data class Suggested<T>(override val instance: T): One<T>, Pseudo<T>
@JvmRecord
data class Actual<T>(val uuid: UUID, override val instance: T): One<T>
}
class None<T> private constructor(): ListElem<T> {
companion object {
private val INSTANCE = None<Any>()
operator fun <T> invoke(): None<T> {
return INSTANCE as None<T>
}
}
}
class Download<T> private constructor(): ListElem<T>, Pseudo<T> {
companion object {
private val INSTANCE = Download<Any>()
operator fun <T> invoke(): Download<T> {
return INSTANCE as Download<T>
}
}
}
class FromDisk<T> private constructor(): ListElem<T>, Pseudo<T> {
companion object {
private val INSTANCE = FromDisk<Any>()
operator fun <T> invoke(): FromDisk<T> {
return INSTANCE as FromDisk<T>
}
}
}
data class Pending<T>(val elems: Flow<ListElem<T>>): ListElem<T>
companion object {
private val fetchGroup = listOf<ListElem<Any>>(Download(), FromDisk())
fun <T> fetchGroup() = fetchGroup as List<ListElem<T>>
}
}
@JvmRecord
data class Separator<T>(val text: String, val line: Boolean) : ListElemIn<T>
fun <T> Pair<UUID, T>.asActual() = ListElem.One.Actual(first, second)
fun <T> T.asSuggested() = ListElem.One.Suggested(this)
fun <T> Flow<T>.asPending() = ListElem.Pending(map { it.asSuggested() })

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T>(
context: ZBContext<T>,
selected: ListElem<T>?,
onItemSelected: Consumer<ListElem<T>>,
) : ComboBoxPopup<ListElem<T>>(context, selected, onItemSelected)
open class ZBComboBox<T>(model: ZBModel<T>, renderer: (() -> ZBModel<T>)-> ZBCellRenderer<T>): ComboBox<ListElem<T>>(model) {
init {
setRenderer(renderer { model })
}
var selectedUUID: UUID?
set(value) {
if (value == null) {
selectedItem = ListElem.None
return
}
for (i in 0..<model.size) {
val element = model.getElementAt(i)
if (element is ListElem.One.Actual) {
if (element.uuid == value) {
selectedIndex = i
return
}
}
}
selectedItem = ListElem.None
}
get() {
val item = selectedItem
return when(item) {
is ListElem.One.Actual<*> -> item.uuid
else -> null
}
}
}
class ZBModel<T> private constructor(elements: List<ListElem<T>>, private var separators: MutableMap<ListElem<T>, Separator<T>>) : CollectionComboBoxModel<ListElem<T>>(elements) {
private var counter: Int = 0
companion object {
operator fun <T> invoke(input: List<ListElemIn<T>>): ZBModel<T> {
val (elements, separators) = convert(input)
val model = ZBModel<T>(elements, separators)
model.launchPendingResolve()
return model
}
private fun <T> convert(input: List<ListElemIn<T>>): Pair<List<ListElem<T>>, MutableMap<ListElem<T>, Separator<T>>> {
val separators = IdentityHashMap<ListElem<T>, Separator<T>>()
var lastSeparator: Separator<T>? = null
val elements = ArrayList<ListElem<T>>()
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<T>) = separators[elem]
private fun launchPendingResolve() {
runInEdt(ModalityState.any()) {
val counter = this.counter
val size = this.size
for (i in 0..<size) {
val elem = getElementAt(i)
?: continue
if (elem !is ListElem.Pending)
continue
zigCoroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) {
elem.elems.collect { newElem ->
insertBefore(elem, newElem, counter)
}
remove(elem, counter)
}
}
}
}
@RequiresEdt
private fun remove(old: ListElem<T>, 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<T>, new: ListElem<T>?, 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<ListElemIn<T>>) {
counter++
val (elements, separators) = convert(input)
this.separators = separators
replaceAll(elements)
launchPendingResolve()
}
}
open class ZBContext<T>(private val project: Project?, private val model: ZBModel<T>, private val getRenderer: (() -> ZBModel<T>) -> ZBCellRenderer<T>) : ComboBoxPopup.Context<ListElem<T>> {
override fun getProject(): Project? {
return project
}
override fun getModel(): ZBModel<T> {
return model
}
override fun getRenderer(): ZBCellRenderer<T> {
return getRenderer(::getModel)
}
}
abstract class ZBCellRenderer<T>(val getModel: () -> ZBModel<T>) : ColoredListCellRenderer<ListElem<T>>() {
final override fun getListCellRendererComponent(
list: JList<out ListElem<T>?>?,
value: ListElem<T>?,
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<out ListElem<T>?>,
value: ListElem<T>?,
index: Int,
selected: Boolean,
hasFocus: Boolean
)
}
private val EMPTY_ICON = EmptyIcon.create(1, 16)

View file

@ -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.no-builds=No builds currently in progress
build.tool.window.status.timeout=zig build -l timed out after {0} seconds. build.tool.window.status.timeout=zig build -l timed out after {0} seconds.
zig=Zig 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.project.display-name=Zig
settings.toolchain.base.name.label=Name settings.toolchain.base.name.label=Name
settings.toolchain.local.path.label=Toolchain location settings.toolchain.local.path.label=Toolchain location
settings.toolchain.local.version.label=Detected zig version settings.toolchain.local.version.label=Detected zig version
settings.toolchain.local.std.label=Override standard library settings.toolchain.local.std.label=Override standard library
settings.toolchain.editor.display-name=Zig
settings.toolchain.editor.toolchain.label=Toolchain settings.toolchain.editor.toolchain.label=Toolchain
settings.toolchain.editor.toolchain-default.label=Default toolchain settings.toolchain.editor.toolchain-default.label=Default toolchain
settings.toolchain.editor.toolchain.edit-button.name=Edit settings.toolchain.editor.toolchain.edit-button.name=Edit
settings.toolchain.model.detected.separator=Detected toolchains settings.toolchain.model.detected.separator=Detected toolchains
settings.toolchain.model.none.text=<No Toolchain> settings.toolchain.model.none.text=<No Toolchain>
settings.toolchain.model.loading.text=Loading\u2026
settings.toolchain.model.from-disk.text=Add Zig from disk\u2026 settings.toolchain.model.from-disk.text=Add Zig from disk\u2026
settings.toolchain.model.download.text=Download Zig\u2026 settings.toolchain.model.download.text=Download Zig\u2026
settings.toolchain.list.title=Toolchains 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.title=Install Zig
settings.toolchain.downloader.version.label=Version: settings.toolchain.downloader.version.label=Version:
settings.toolchain.downloader.location.label=Location: settings.toolchain.downloader.location.label=Location:

View file

@ -24,20 +24,18 @@ package com.falsepattern.zigbrains.lsp
import com.falsepattern.zigbrains.lsp.config.SuspendingZLSConfigProvider import com.falsepattern.zigbrains.lsp.config.SuspendingZLSConfigProvider
import com.falsepattern.zigbrains.lsp.config.ZLSConfig import com.falsepattern.zigbrains.lsp.config.ZLSConfig
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import kotlin.io.path.pathString import kotlin.io.path.pathString
class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider { class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider {
override suspend fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { override suspend fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig {
val svc = project.zigProjectSettings val svc = ZigToolchainService.getInstance(project)
var state = svc.state val toolchain = svc.toolchain ?: return previous
val toolchain = state.toolchain ?: project.suggestZigToolchain(UserDataHolderBase()) ?: return previous
val env = toolchain.zig.getEnv(project).getOrElse { throwable -> val env = toolchain.zig.getEnv(project).getOrElse { throwable ->
throwable.printStackTrace() throwable.printStackTrace()
@ -65,16 +63,10 @@ class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider {
).notify(project) ).notify(project)
return previous return previous
} }
var lib = if (state.overrideStdPath && state.explicitPathToStd != null) { var lib = if (toolchain is LocalZigToolchain)
state.explicitPathToStd?.toNioPathOrNull() ?: run { toolchain.std
Notification( else
"zigbrains-lsp", null
"Invalid zig standard library path override: ${state.explicitPathToStd}",
NotificationType.ERROR
).notify(project)
null
}
} else null
if (lib == null) { if (lib == null) {
lib = env.libDirectory.toNioPathOrNull() ?: run { lib = env.libDirectory.toNioPathOrNull() ?: run {

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Project> {
return ZLSSettingsConfigurable(project)
}
override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel {
return ZLSSettingsPanel(ProjectManager.getInstance().defaultProject)
}
override val priority: Int
get() = 1000
}

View file

@ -22,36 +22,19 @@
package com.falsepattern.zigbrains.lsp 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.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.startup.ProjectActivity
import com.intellij.ui.EditorNotifications import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.io.path.pathString
class ZLSStartup: ProjectActivity { class ZLSStartup: ProjectActivity {
override suspend fun execute(project: Project) { 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 { project.zigCoroutineScope.launch {
var currentState = project.zlsRunningAsync() var currentState = project.zlsRunning()
while (!project.isDisposed) { while (!project.isDisposed) {
val running = project.zlsRunningAsync() val running = project.zlsRunning()
if (currentState != running) { if (currentState != running) {
EditorNotifications.getInstance(project).updateAllNotifications() EditorNotifications.getInstance(project).updateAllNotifications()
} }

View file

@ -22,11 +22,8 @@
package com.falsepattern.zigbrains.lsp 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.config.ZLSConfigProviderBase
import com.falsepattern.zigbrains.lsp.settings.zlsSettings import com.falsepattern.zigbrains.lsp.zls.ZLSService
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
@ -55,30 +52,9 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
suspend fun getCommand(project: Project): List<String>? { suspend fun getCommand(project: Project): List<String>? {
val svc = project.zlsSettings val svc = ZLSService.getInstance(project)
val state = svc.state val zls = svc.zls ?: return null
val zlsPath: Path = state.zlsPath.let { zlsPath -> val zlsPath: Path = zls.path
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
}
}
}
if (!zlsPath.toFile().exists()) { if (!zlsPath.toFile().exists()) {
Notification( Notification(
"zigbrains-lsp", "zigbrains-lsp",
@ -95,7 +71,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje
).notify(project) ).notify(project)
return null return null
} }
val configPath: Path? = state.zlsConfigPath.let { configPath -> val configPath: Path? = "".let { configPath ->
if (configPath.isNotBlank()) { if (configPath.isNotBlank()) {
configPath.toNioPathOrNull()?.let { nioPath -> configPath.toNioPathOrNull()?.let { nioPath ->
if (!nioPath.toFile().exists()) { if (!nioPath.toFile().exists()) {

View file

@ -22,7 +22,7 @@
package com.falsepattern.zigbrains.lsp 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.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
@ -68,39 +68,29 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS
} }
features.inlayHintFeature = object: LSPInlayHintFeature() { features.inlayHintFeature = object: LSPInlayHintFeature() {
override fun isEnabled(file: PsiFile): Boolean { override fun isEnabled(file: PsiFile): Boolean {
return features.project.zlsSettings.state.inlayHints return ZLSService.getInstance(project).zls?.settings?.inlayHints == true
} }
} }
return features return features
} }
override fun isEnabled(project: Project) = project.zlsEnabledSync() override fun isEnabled(project: Project) = project.zlsEnabled()
override fun setEnabled(enabled: Boolean, project: Project) { override fun setEnabled(enabled: Boolean, project: Project) {
project.zlsEnabled(enabled) project.zlsEnabled(enabled)
} }
} }
suspend fun Project.zlsEnabledAsync(): Boolean { fun Project.zlsEnabled(): Boolean {
return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateAsync() return (getUserData(ENABLED_KEY) != false) && ZLSService.getInstance(this).zls?.isValid() == true
}
fun Project.zlsEnabledSync(): Boolean {
return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateSync()
} }
fun Project.zlsEnabled(value: Boolean) { fun Project.zlsEnabled(value: Boolean) {
putUserData(ENABLED_KEY, value) putUserData(ENABLED_KEY, value)
} }
suspend fun Project.zlsRunningAsync(): Boolean { fun Project.zlsRunning(): Boolean {
if (!zlsEnabledAsync()) if (!zlsEnabled())
return false
return lsm.isRunning
}
fun Project.zlsRunningSync(): Boolean {
if (!zlsEnabledSync())
return false return false
return lsm.isRunning return lsm.isRunning
} }
@ -135,7 +125,7 @@ private suspend fun doStart(project: Project, restart: Boolean) {
project.lsm.stop("ZigBrains") project.lsm.stop("ZigBrains")
delay(250) delay(250)
} }
if (project.zlsSettings.validateAsync()) { if (ZLSService.getInstance(project).zls?.isValid() == true) {
delay(250) delay(250)
project.lsm.start("ZigBrains") project.lsm.start("ZigBrains")
} }

View file

@ -23,8 +23,8 @@
package com.falsepattern.zigbrains.lsp.notification package com.falsepattern.zigbrains.lsp.notification
import com.falsepattern.zigbrains.lsp.ZLSBundle import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.settings.zlsSettings import com.falsepattern.zigbrains.lsp.zls.ZLSService
import com.falsepattern.zigbrains.lsp.zlsRunningAsync import com.falsepattern.zigbrains.lsp.zlsRunning
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.falsepattern.zigbrains.zig.ZigFileType import com.falsepattern.zigbrains.zig.ZigFileType
import com.falsepattern.zigbrains.zon.ZonFileType import com.falsepattern.zigbrains.zon.ZonFileType
@ -49,10 +49,10 @@ class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware {
else -> return null else -> return null
} }
val task = project.zigCoroutineScope.async { val task = project.zigCoroutineScope.async {
if (project.zlsRunningAsync()) { if (project.zlsRunning()) {
return@async null return@async null
} else { } else {
return@async project.zlsSettings.validateAsync() return@async ZLSService.getInstance(project).zls?.isValid() == true
} }
} }
return Function { editor -> return Function { editor ->

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZLSSettings> {
@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<ZLSProjectSettingsService>()

View file

@ -25,35 +25,31 @@ package com.falsepattern.zigbrains.lsp.settings
import com.falsepattern.zigbrains.lsp.config.SemanticTokens import com.falsepattern.zigbrains.lsp.config.SemanticTokens
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.annotations.Attribute
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
@Suppress("PropertyName") @Suppress("PropertyName")
data class ZLSSettings( data class ZLSSettings(
var zlsPath: @NonNls String = "", @JvmField @Attribute val zlsConfigPath: @NonNls String = "",
var zlsConfigPath: @NonNls String = "", @JvmField @Attribute val inlayHints: Boolean = true,
val inlayHints: Boolean = true, @JvmField @Attribute val enable_snippets: Boolean = true,
val enable_snippets: Boolean = true, @JvmField @Attribute val enable_argument_placeholders: Boolean = true,
val enable_argument_placeholders: Boolean = true, @JvmField @Attribute val completion_label_details: Boolean = true,
val completion_label_details: Boolean = true, @JvmField @Attribute val enable_build_on_save: Boolean = false,
val enable_build_on_save: Boolean = false, @JvmField @Attribute val build_on_save_args: String = "",
val build_on_save_args: String = "", @JvmField @Attribute val semantic_tokens: SemanticTokens = SemanticTokens.full,
val semantic_tokens: SemanticTokens = SemanticTokens.full, @JvmField @Attribute val inlay_hints_show_variable_type_hints: Boolean = true,
val inlay_hints_show_variable_type_hints: Boolean = true, @JvmField @Attribute val inlay_hints_show_struct_literal_field_type: Boolean = true,
val inlay_hints_show_struct_literal_field_type: Boolean = true, @JvmField @Attribute val inlay_hints_show_parameter_name: Boolean = true,
val inlay_hints_show_parameter_name: Boolean = true, @JvmField @Attribute val inlay_hints_show_builtin: Boolean = true,
val inlay_hints_show_builtin: Boolean = true, @JvmField @Attribute val inlay_hints_exclude_single_argument: Boolean = true,
val inlay_hints_exclude_single_argument: Boolean = true, @JvmField @Attribute val inlay_hints_hide_redundant_param_names: Boolean = false,
val inlay_hints_hide_redundant_param_names: Boolean = false, @JvmField @Attribute val inlay_hints_hide_redundant_param_names_last_token: Boolean = false,
val inlay_hints_hide_redundant_param_names_last_token: Boolean = false, @JvmField @Attribute val warn_style: Boolean = false,
val warn_style: Boolean = false, @JvmField @Attribute val highlight_global_var_declarations: Boolean = false,
val highlight_global_var_declarations: Boolean = false, @JvmField @Attribute val skip_std_references: Boolean = false,
val skip_std_references: Boolean = false, @JvmField @Attribute val prefer_ast_check_as_child_process: Boolean = true,
val prefer_ast_check_as_child_process: Boolean = true, @JvmField @Attribute val builtin_path: String? = null,
val builtin_path: String? = null, @JvmField @Attribute val build_runner_path: @NonNls String? = null,
val build_runner_path: @NonNls String? = null, @JvmField @Attribute val global_cache_path: @NonNls String? = null,
val global_cache_path: @NonNls String? = null, )
): ZigProjectConfigurationProvider.Settings {
override fun apply(project: Project) {
project.zlsSettings.loadState(this)
}
}

View file

@ -24,12 +24,13 @@ package com.falsepattern.zigbrains.lsp.settings
import com.falsepattern.zigbrains.lsp.config.ZLSConfig import com.falsepattern.zigbrains.lsp.config.ZLSConfig
import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider
import com.falsepattern.zigbrains.lsp.zls.ZLSService
import com.falsepattern.zigbrains.shared.cli.translateCommandline import com.falsepattern.zigbrains.shared.cli.translateCommandline
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
class ZLSSettingsConfigProvider: ZLSConfigProvider { class ZLSSettingsConfigProvider: ZLSConfigProvider {
override fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { 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( return previous.copy(
enable_snippets = state.enable_snippets, enable_snippets = state.enable_snippets,
enable_argument_placeholders = state.enable_argument_placeholders, enable_argument_placeholders = state.enable_argument_placeholders,

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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()
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<UUID>() {
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()
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZLSVersion, ZLSVersion.Ref, MyState>(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<ZLSInstallationsService>()
}
}
inline val zlsInstallations: ZLSInstallationsService get() = ZLSInstallationsService.getInstance()
private typealias ZLSStorage = UUIDStorage<ZLSVersion.Ref>

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZLSVersion>() {
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")
}
}

View file

@ -0,0 +1,63 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.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<ZLSService.MyState>(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<ZLSService>()
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZLSVersion> {
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) }
}
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZLSVersion> {
override val theMap: UUIDMapSerializable.Converting<ZLSVersion, *, *>
get() = zlsInstallations
override fun constructModelList(): List<ListElemIn<ZLSVersion>> {
return ListElem.fetchGroup()
}
override fun createContext(model: ZBModel<ZLSVersion>): ZBContext<ZLSVersion> {
return ZLSContext(null, model)
}
override fun createComboBox(model: ZBModel<ZLSVersion>): ZBComboBox<ZLSVersion> {
return ZLSComboBox(model)
}
override suspend fun resolvePseudo(
context: Component,
elem: ListElem.Pseudo<ZLSVersion>
): UUID? {
//TODO
return null
}
override fun createNamedConfigurable(uuid: UUID, elem: ZLSVersion): NamedConfigurable<UUID> {
//TODO
return ZLSConfigurable(uuid, elem)
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZLSVersion>(ZLSDriver),
SubConfigurable<Project>,
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<Project>? {
return ZLSEditor(project, sharedState)
}
override val index: Int
get() = 50
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZLSVersion>(ZLSDriver) {
override fun getEmptySelectionString(): String {
return ZLSBundle.message("settings.list.empty")
}
override fun getDisplayName(): @NlsContexts.ConfigurableName String? {
return ZLSBundle.message("settings.list.title")
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<ZLSVersion>): ZBComboBox<ZLSVersion>(model, ::ZLSCellRenderer)
class ZLSContext(project: Project?, model: ZBModel<ZLSVersion>): ZBContext<ZLSVersion>(project, model, ::ZLSCellRenderer)
class ZLSCellRenderer(getModel: () -> ZBModel<ZLSVersion>): ZBCellRenderer<ZLSVersion>(getModel) {
override fun customizeCellRenderer(
list: JList<out ListElem<ZLSVersion>?>,
value: ListElem<ZLSVersion>?,
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)
}
}
}
}

View file

@ -49,6 +49,13 @@
<editorNotificationProvider <editorNotificationProvider
implementation="com.falsepattern.zigbrains.lsp.notification.ZigEditorNotificationProvider" implementation="com.falsepattern.zigbrains.lsp.notification.ZigEditorNotificationProvider"
/> />
<applicationConfigurable
parentId="ZigConfigurable"
instance="com.falsepattern.zigbrains.lsp.zls.ui.ZLSListEditor"
id="ZLSListEditor"
bundle="zigbrains.lsp.Bundle"
key="settings.list.title"
/>
</extensions> </extensions>
<extensions defaultExtensionNs="com.falsepattern.zigbrains"> <extensions defaultExtensionNs="com.falsepattern.zigbrains">
@ -59,7 +66,7 @@
implementation="com.falsepattern.zigbrains.lsp.ToolchainZLSConfigProvider" implementation="com.falsepattern.zigbrains.lsp.ToolchainZLSConfigProvider"
/> />
<projectConfigProvider <projectConfigProvider
implementation="com.falsepattern.zigbrains.lsp.ZLSProjectConfigurationProvider" implementation="com.falsepattern.zigbrains.lsp.zls.ui.ZLSEditor$Provider"
/> />
</extensions> </extensions>

View file

@ -66,3 +66,10 @@ progress.title.validate=Validating ZLS
lsp.zls.name=Zig Language Server lsp.zls.name=Zig Language Server
# suppress inspection "UnusedProperty" # suppress inspection "UnusedProperty"
lsp.zls.description=The <a href="https://github.com/Zigtools/ZLS">Zig Language Server</a>, via ZigBrains lsp.zls.description=The <a href="https://github.com/Zigtools/ZLS">Zig Language Server</a>, 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=<No ZLS>
settings.model.loading.text=Loading\u2026
settings.model.from-disk.text=Add ZLS from disk\u2026
settings.model.download.text=Download ZLS\u2026

View file

@ -17,6 +17,11 @@
dynamic="true" dynamic="true"
name="zlsConfigProvider" name="zlsConfigProvider"
/> />
<extensionPoint
interface="com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainExtensionsProvider"
dynamic="true"
name="toolchainExtensionsProvider"
/>
<extensionPoint <extensionPoint
interface="com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider" interface="com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider"
dynamic="true" dynamic="true"