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 {
// includeModule("com.redhat.devtools.intellij", "lsp4ij")
includeModule("com.redhat.devtools.intellij", "lsp4ij")
}
}
mavenCentral()
@ -104,12 +104,12 @@ dependencies {
pluginVerifier()
zipSigner()
// plugin(lsp4ijPluginString)
plugin(lsp4ijPluginString)
}
runtimeOnly(project(":core"))
runtimeOnly(project(":cidr"))
// runtimeOnly(project(":lsp"))
runtimeOnly(project(":lsp"))
}
intellijPlatform {

View file

@ -92,6 +92,6 @@ abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationP
}
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.util.NlsActions.ActionText
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.util.progress.reportRawProgress
import org.jdom.Element
import org.jetbrains.annotations.Nls

View file

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

View file

@ -42,8 +42,8 @@ import java.util.UUID
name = "ZigToolchain",
storages = [Storage("zigbrains.xml")]
)
class ZigToolchainService(val project: Project): SerializablePersistentStateComponent<ZigToolchainService.State>(State()), IZigToolchainService {
override var toolchainUUID: UUID?
class ZigToolchainService(private val project: Project): SerializablePersistentStateComponent<ZigToolchainService.State>(State()) {
var toolchainUUID: UUID?
get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf {
if (it in zigToolchainList) {
true
@ -63,7 +63,7 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp
}
}
override val toolchain: ZigToolchain?
val toolchain: ZigToolchain?
get() = toolchainUUID?.let { zigToolchainList[it] }
data class State(
@ -74,11 +74,6 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp
companion object {
@JvmStatic
fun getInstance(project: Project): IZigToolchainService = project.service<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
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.NlsContexts
@ -38,14 +39,14 @@ abstract class ZigToolchainConfigurable<T: ZigToolchain>(
zigToolchainList[uuid] = 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? {
var views = myViews
if (views.isEmpty()) {
views = ArrayList<ZigToolchainPanel<T>>()
views = ArrayList<ImmutableElementPanel<T>>()
views.add(createPanel())
views.addAll(createZigToolchainExtensionPanels())
views.forEach { it.reset(toolchain) }

View file

@ -22,16 +22,17 @@
package com.falsepattern.zigbrains.project.toolchain.base
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.intellij.openapi.extensions.ExtensionPointName
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainExtensionsProvider>("com.falsepattern.zigbrains.toolchainExtensionsProvider")
internal interface ZigToolchainExtensionsProvider {
fun <T : ZigToolchain> createExtensionPanel(): ZigToolchainPanel<T>?
fun <T : ZigToolchain> createExtensionPanel(): ImmutableElementPanel<T>?
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 {
it.createExtensionPanel()
}

View file

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

View file

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

View file

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

View file

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

View file

@ -20,17 +20,17 @@
* 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.ui.dsl.builder.Panel
interface ZigToolchainPanel<T: ZigToolchain>: Disposable {
interface ImmutableElementPanel<T>: Disposable {
fun attach(p: Panel)
fun isModified(toolchain: T): Boolean
fun isModified(elem: T): Boolean
/**
* Returned object must be the exact same class as the provided one.
*/
fun apply(toolchain: T): T?
fun reset(toolchain: T)
fun apply(elem: T): T?
fun reset(elem: T)
}

View file

@ -20,17 +20,15 @@
* 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.intellij.openapi.Disposable
import com.falsepattern.zigbrains.shared.NamedObject
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.util.preferredHeight
import java.awt.Dimension
abstract class ZigToolchainPanelBase<T: ZigToolchain>: ZigToolchainPanel<T> {
abstract class ImmutableNamedElementPanelBase<T>: ImmutableElementPanel<T> {
private val nameField = JBTextField(25)
protected var nameFieldValue: String?

View file

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

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.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.StorageChangeListener
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.application.EDT
import com.intellij.openapi.observable.util.whenListChanged
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.awt.event.ItemEvent
import java.util.UUID
import javax.swing.JButton
import kotlin.collections.addAll
class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>, ZigProjectConfigurationProvider.UserDataListener {
private val toolchainBox: TCComboBox
private var selectOnNextReload: UUID? = null
private val model: TCModel
private var editButton: JButton? = null
private val changeListener: StorageChangeListener = { this@ZigToolchainEditor.toolchainListChanged() }
class ZigToolchainEditor(private var project: Project?,
private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge):
UUIDMapSelector<ZigToolchain>(ZigToolchainDriver.ForSelector(project, sharedState)),
SubConfigurable<Project>,
ZigProjectConfigurationProvider.UserDataListener
{
init {
model = TCModel(getModelList(project, sharedState))
toolchainBox = TCComboBox(model)
toolchainBox.addItemListener(::itemStateChanged)
zigToolchainList.addChangeListener(changeListener)
sharedState.addUserDataChangeListener(this)
model.whenListChanged {
if (toolchainBox.isPopupVisible) {
toolchainBox.isPopupVisible = false
toolchainBox.isPopupVisible = true
}
}
}
private fun refreshButtonState(item: Any?) {
editButton?.isEnabled = item is TCListElem.Toolchain.Actual
editButton?.repaint()
}
private fun itemStateChanged(event: ItemEvent) {
if (event.stateChange != ItemEvent.SELECTED) {
return
}
val item = event.item
refreshButtonState(item)
if (item !is TCListElem.Pseudo)
return
zigCoroutineScope.launch(toolchainBox.asContextElement()) {
val uuid = runCatching { ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) }.getOrNull()
delay(100)
withEDTContext(toolchainBox.asContextElement()) {
applyUUIDNowOrOnReload(uuid)
}
}
}
private suspend fun toolchainListChanged() {
withContext(Dispatchers.EDT + toolchainBox.asContextElement()) {
val list = getModelList(project, sharedState)
model.updateContents(list)
val onReload = selectOnNextReload
selectOnNextReload = null
if (onReload != null) {
val element = list.firstOrNull { when(it) {
is TCListElem.Toolchain.Actual -> it.uuid == onReload
else -> false
} }
model.selectedItem = element
return@withContext
}
val selected = model.selected
if (selected != null && list.contains(selected)) {
model.selectedItem = selected
return@withContext
}
if (selected is TCListElem.Toolchain.Actual) {
val uuid = selected.uuid
val element = list.firstOrNull { when(it) {
is TCListElem.Toolchain.Actual -> it.uuid == uuid
else -> false
} }
model.selectedItem = element
return@withContext
}
model.selectedItem = TCListElem.None
}
}
override fun onUserDataChanged(key: Key<*>) {
zigCoroutineScope.launch { toolchainListChanged() }
zigCoroutineScope.launch { listChanged() }
}
@ -141,50 +57,26 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
else
"settings.toolchain.editor.toolchain.label")
) {
cell(toolchainBox).resizableColumn().align(AlignX.FILL)
button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e ->
zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) {
var selectedUUID = toolchainBox.selectedToolchain ?: return@launchWithEDT
val toolchain = zigToolchainList[selectedUUID] ?: return@launchWithEDT
val config = toolchain.createNamedConfigurable(selectedUUID)
val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config)
if (apply) {
applyUUIDNowOrOnReload(selectedUUID)
}
}
}.component.let {
editButton = it
refreshButtonState(toolchainBox.selectedItem)
}
}
}
@RequiresEdt
private fun applyUUIDNowOrOnReload(uuid: UUID?) {
toolchainBox.selectedToolchain = uuid
if (uuid != null && toolchainBox.selectedToolchain == null) {
selectOnNextReload = uuid
} else {
selectOnNextReload = null
attachComboBoxRow(this)
}
}
override fun isModified(context: Project): Boolean {
return ZigToolchainService.getInstance(context).toolchainUUID != toolchainBox.selectedToolchain
return ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID
}
override fun apply(context: Project) {
ZigToolchainService.getInstance(context).toolchainUUID = toolchainBox.selectedToolchain
ZigToolchainService.getInstance(context).toolchainUUID = selectedUUID
}
override fun reset(context: Project?) {
val project = context ?: ProjectManager.getInstance().defaultProject
toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID
refreshButtonState(toolchainBox.selectedItem)
selectedUUID = ZigToolchainService.getInstance(project).toolchainUUID
}
override fun dispose() {
zigToolchainList.removeChangeListener(changeListener)
super.dispose()
sharedState.removeUserDataChangeListener(this)
}
override val newProjectBeforeInitSelector get() = true
@ -196,15 +88,3 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
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
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.StorageChangeListener
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.observable.util.whenListChanged
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.MasterDetailsComponent
import com.intellij.util.IconUtil
import com.intellij.util.asSafely
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.launch
import java.util.UUID
import javax.swing.JComponent
import javax.swing.tree.DefaultTreeModel
class ZigToolchainListEditor : MasterDetailsComponent() {
private var isTreeInitialized = false
private var registered: Boolean = false
private var selectOnNextReload: UUID? = null
private val changeListener: StorageChangeListener = { this@ZigToolchainListEditor.toolchainListChanged() }
override fun createComponent(): JComponent {
if (!isTreeInitialized) {
initTree()
isTreeInitialized = true
}
if (!registered) {
zigToolchainList.addChangeListener(changeListener)
registered = true
}
return super.createComponent()
}
override fun createActions(fromPopup: Boolean): List<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")
import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor
class ZigToolchainListEditor : UUIDMapEditor<ZigToolchain>(ZigToolchainDriver.ForList) {
override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title")
override fun disposeUIResources() {
super.disposeUIResources()
if (registered) {
zigToolchainList.removeChangeListener(changeListener)
}
}
private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) {
val node = MyNode(toolchain.createNamedConfigurable(uuid))
addNode(node, myRoot)
}
private fun reloadTree() {
val currentSelection = selectedObject?.asSafely<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.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.render
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer
import com.falsepattern.zigbrains.shared.ui.ZBComboBox
import com.falsepattern.zigbrains.shared.ui.ZBContext
import com.falsepattern.zigbrains.shared.ui.ZBModel
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.CellRendererPanel
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.GroupHeaderSeparator
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.panels.OpaquePanel
import com.intellij.ui.popup.list.ComboBoxPopup
import com.intellij.util.Consumer
import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.ui.EmptyIcon
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.awt.BorderLayout
import java.awt.Component
import java.util.IdentityHashMap
import java.util.UUID
import javax.accessibility.AccessibleContext
import com.intellij.ui.icons.EMPTY_ICON
import javax.swing.JList
import javax.swing.border.Border
internal class TCComboBoxPopup(
context: TCContext,
selected: TCListElem?,
onItemSelected: Consumer<TCListElem>,
) : ComboBoxPopup<TCListElem>(context, selected, onItemSelected)
class TCComboBox(model: ZBModel<ZigToolchain>): ZBComboBox<ZigToolchain>(model, ::TCCellRenderer)
internal class TCComboBox(model: TCModel): ComboBox<TCListElem>(model) {
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 TCContext(project: Project?, model: ZBModel<ZigToolchain>): ZBContext<ZigToolchain>(project, model, ::TCCellRenderer)
class TCCellRenderer(getModel: () -> ZBModel<ZigToolchain>): ZBCellRenderer<ZigToolchain>(getModel) {
override fun customizeCellRenderer(
list: JList<out TCListElem?>,
value: TCListElem?,
list: JList<out ListElem<ZigToolchain>?>,
value: ListElem<ZigToolchain>?,
index: Int,
selected: Boolean,
hasFocus: Boolean
) {
icon = EMPTY_ICON
when (value) {
is TCListElem.Toolchain -> {
is ListElem.One -> {
val (icon, isSuggestion) = when(value) {
is TCListElem.Toolchain.Suggested -> AllIcons.General.Information to true
is TCListElem.Toolchain.Actual -> Icons.Zig to false
is ListElem.One.Suggested -> AllIcons.General.Information to true
is ListElem.One.Actual -> Icons.Zig to false
}
this.icon = icon
val toolchain = value.toolchain
toolchain.render(this, isSuggestion, index == -1)
val item = value.instance
item.render(this, isSuggestion, index == -1)
}
is TCListElem.Download -> {
is ListElem.Download -> {
icon = AllIcons.Actions.Download
append(ZigBrainsBundle.message("settings.toolchain.model.download.text"))
}
is TCListElem.FromDisk -> {
is ListElem.FromDisk -> {
icon = AllIcons.General.OpenDisk
append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text"))
}
is TCListElem.Pending -> {
is ListElem.Pending -> {
icon = AllIcons.Empty
append("Loading\u2026", SimpleTextAttributes.GRAYED_ATTRIBUTES)
append(ZigBrainsBundle.message("settings.toolchain.model.loading.text"), SimpleTextAttributes.GRAYED_ATTRIBUTES)
}
is TCListElem.None, null -> {
is ListElem.None, null -> {
icon = AllIcons.General.BalloonError
append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES)
}
}
}
}
private val EMPTY_ICON = EmptyIcon.create(1, 16)
}

View file

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

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.timeout=zig build -l timed out after {0} seconds.
zig=Zig
settings.shared.list.add-action.name=Add New
settings.shared.list.empty=Select an entry to view or edit its details here
settings.project.display-name=Zig
settings.toolchain.base.name.label=Name
settings.toolchain.local.path.label=Toolchain location
settings.toolchain.local.version.label=Detected zig version
settings.toolchain.local.std.label=Override standard library
settings.toolchain.editor.display-name=Zig
settings.toolchain.editor.toolchain.label=Toolchain
settings.toolchain.editor.toolchain-default.label=Default toolchain
settings.toolchain.editor.toolchain.edit-button.name=Edit
settings.toolchain.model.detected.separator=Detected toolchains
settings.toolchain.model.none.text=<No Toolchain>
settings.toolchain.model.loading.text=Loading\u2026
settings.toolchain.model.from-disk.text=Add Zig from disk\u2026
settings.toolchain.model.download.text=Download Zig\u2026
settings.toolchain.list.title=Toolchains
settings.toolchain.list.add-action.name=Add New
settings.toolchain.list.empty=Select a toolchain to view or edit its details here
settings.toolchain.downloader.title=Install Zig
settings.toolchain.downloader.version.label=Version:
settings.toolchain.downloader.location.label=Location:

View file

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

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
import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
import com.falsepattern.zigbrains.lsp.settings.zlsSettings
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.io.path.pathString
class ZLSStartup: ProjectActivity {
override suspend fun execute(project: Project) {
val zlsState = project.zlsSettings.state
if (zlsState.zlsPath.isBlank()) {
val env = if (DirenvCmd.direnvInstalled() && !project.isDefault && project.zigProjectSettings.state.direnv)
project.getDirenv()
else
emptyEnv
env.findExecutableOnPATH("zls")?.let {
zlsState.zlsPath = it.pathString
project.zlsSettings.state = zlsState
}
}
project.zigCoroutineScope.launch {
var currentState = project.zlsRunningAsync()
var currentState = project.zlsRunning()
while (!project.isDisposed) {
val running = project.zlsRunningAsync()
val running = project.zlsRunning()
if (currentState != running) {
EditorNotifications.getInstance(project).updateAllNotifications()
}

View file

@ -22,11 +22,8 @@
package com.falsepattern.zigbrains.lsp
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
import com.falsepattern.zigbrains.lsp.config.ZLSConfigProviderBase
import com.falsepattern.zigbrains.lsp.settings.zlsSettings
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.lsp.zls.ZLSService
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
@ -55,30 +52,9 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje
@OptIn(ExperimentalSerializationApi::class)
suspend fun getCommand(project: Project): List<String>? {
val svc = project.zlsSettings
val state = svc.state
val zlsPath: Path = state.zlsPath.let { zlsPath ->
if (zlsPath.isEmpty()) {
val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv
env.findExecutableOnPATH("zls") ?: run {
Notification(
"zigbrains-lsp",
ZLSBundle.message("notification.message.could-not-detect.content"),
NotificationType.ERROR
).notify(project)
return null
}
} else {
zlsPath.toNioPathOrNull() ?: run {
Notification(
"zigbrains-lsp",
ZLSBundle.message("notification.message.zls-exe-path-invalid.content", zlsPath),
NotificationType.ERROR
).notify(project)
return null
}
}
}
val svc = ZLSService.getInstance(project)
val zls = svc.zls ?: return null
val zlsPath: Path = zls.path
if (!zlsPath.toFile().exists()) {
Notification(
"zigbrains-lsp",
@ -95,7 +71,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje
).notify(project)
return null
}
val configPath: Path? = state.zlsConfigPath.let { configPath ->
val configPath: Path? = "".let { configPath ->
if (configPath.isNotBlank()) {
configPath.toNioPathOrNull()?.let { nioPath ->
if (!nioPath.toFile().exists()) {

View file

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

View file

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

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

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.ZLSConfigProvider
import com.falsepattern.zigbrains.lsp.zls.ZLSService
import com.falsepattern.zigbrains.shared.cli.translateCommandline
import com.intellij.openapi.project.Project
class ZLSSettingsConfigProvider: ZLSConfigProvider {
override fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig {
val state = project.zlsSettings.state
val state = ZLSService.getInstance(project).zls?.settings ?: return previous
return previous.copy(
enable_snippets = state.enable_snippets,
enable_argument_placeholders = state.enable_argument_placeholders,

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
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 defaultExtensionNs="com.falsepattern.zigbrains">
@ -59,7 +66,7 @@
implementation="com.falsepattern.zigbrains.lsp.ToolchainZLSConfigProvider"
/>
<projectConfigProvider
implementation="com.falsepattern.zigbrains.lsp.ZLSProjectConfigurationProvider"
implementation="com.falsepattern.zigbrains.lsp.zls.ui.ZLSEditor$Provider"
/>
</extensions>

View file

@ -66,3 +66,10 @@ progress.title.validate=Validating ZLS
lsp.zls.name=Zig Language Server
# suppress inspection "UnusedProperty"
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"
name="zlsConfigProvider"
/>
<extensionPoint
interface="com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainExtensionsProvider"
dynamic="true"
name="toolchainExtensionsProvider"
/>
<extensionPoint
interface="com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider"
dynamic="true"