From 8336d2bcc58ac2c1bd2befea3bac66b1ae8c8b86 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Fri, 11 Apr 2025 16:47:01 +0200 Subject: [PATCH] toolchain+lsp management feature complete! --- LICENSE | 8 +- .../zigbrains/direnv/DirenvService.kt | 4 +- .../com/falsepattern/zigbrains/direnv/Env.kt | 6 +- .../zigbrains/direnv/ui/DirenvEditor.kt | 5 +- .../base/ZigToolchainConfigurable.kt | 16 +- .../base/ZigToolchainExtensionsProvider.kt | 13 +- .../toolchain/base/ZigToolchainProvider.kt | 6 +- .../downloader/LocalToolchainDownloader.kt | 21 +- .../local/LocalZigToolchainConfigurable.kt | 5 +- .../toolchain/local/LocalZigToolchainPanel.kt | 8 +- .../local/LocalZigToolchainProvider.kt | 41 +-- .../toolchain/ui/ImmutableElementPanel.kt | 2 +- .../toolchain/ui/ZigToolchainDriver.kt | 8 +- .../toolchain/ui/ZigToolchainEditor.kt | 55 +++- .../shared/downloader/LocalSelector.kt | 2 +- .../zigbrains/shared/ui/UUIDComboBoxDriver.kt | 2 +- .../zigbrains/shared/ui/UUIDMapEditor.kt | 36 ++- .../zigbrains/shared/ui/UUIDMapSelector.kt | 29 ++- .../falsepattern/zigbrains/shared/ui/model.kt | 44 +++- licenses/ZLS.LICENSE | 21 ++ .../falsepattern/zigbrains/lsp/LSPIcons.kt | 32 +++ .../lsp/settings/ZLSSettingsPanel.kt | 236 ++++++++++++++++++ .../zigbrains/lsp/zls/ZLSConfigurable.kt | 7 +- .../zigbrains/lsp/zls/ZLSPanel.kt | 20 +- .../zigbrains/lsp/zls/ZLSVersion.kt | 4 - .../lsp/zls/downloader/ZLSDownloader.kt | 64 +---- .../lsp/zls/downloader/ZLSLocalSelector.kt | 9 + .../lsp/zls/downloader/ZLSVersionInfo.kt | 31 ++- .../zigbrains/lsp/zls/ui/ZLSDriver.kt | 185 ++++++++++---- .../zigbrains/lsp/zls/ui/ZLSEditor.kt | 16 +- .../zigbrains/lsp/zls/ui/ZLSListEditor.kt | 1 - .../zigbrains/lsp/zls/ui/model.kt | 16 +- lsp/src/main/resources/icons/zls.svg | 19 ++ .../resources/zigbrains/lsp/Bundle.properties | 2 +- src/art/zls/zls.svg | 19 ++ 35 files changed, 756 insertions(+), 237 deletions(-) create mode 100644 licenses/ZLS.LICENSE create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/LSPIcons.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt create mode 100644 lsp/src/main/resources/icons/zls.svg create mode 100644 src/art/zls/zls.svg diff --git a/LICENSE b/LICENSE index 2d0c7b3b..22ea4295 100644 --- a/LICENSE +++ b/LICENSE @@ -25,6 +25,11 @@ which are the property of the Zig Software Foundation. (https://github.com/ziglang/logo) These art assets are licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0). -------------------------------- +The art assets inside src/art/zls, and all copies of them, are derived from the Zig Language Server, +which are the property of the zigtools organization. +(https://github.com/zigtools/zls) +These art assets are licensed under MIT license. +-------------------------------- Parts of the codebase are based on the intellij-zig plugin, developed by HTGAzureX1212 (https://github.com/HTGAzureX1212), licensed under the Apache 2.0 license. -------------------------------- @@ -37,4 +42,5 @@ All of the licenses listed here are available in the following files, bundled wi - licenses/CC_BY_SA_4.0.LICENSE - licenses/GPL3.LICENSE - licenses/INTELLIJ-RUST.LICENSE -- licenses/LGPL3.LICENSE \ No newline at end of file +- licenses/LGPL3.LICENSE +- licenses/ZLS.LICENSE \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt index 2e8d0807..a56cfa2b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt @@ -143,8 +143,8 @@ class DirenvService(val project: Project): SerializablePersistentStateComponent< private val STATE_KEY = Key.create("DIRENV_STATE") - fun getStateFor(data: UserDataHolder, project: Project?): DirenvState { - return data.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled + fun getStateFor(data: UserDataHolder?, project: Project?): DirenvState { + return data?.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled } fun setStateFor(data: UserDataHolder, state: DirenvState) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt index 2af970ac..42db892a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt @@ -25,8 +25,10 @@ package com.falsepattern.zigbrains.direnv import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.util.EnvironmentUtil +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import org.jetbrains.annotations.NonNls import java.io.File import kotlin.io.path.absolute @@ -41,8 +43,6 @@ data class Env(val env: Map) { private fun getVariable(name: @NonNls String) = env.getOrElse(name) { EnvironmentUtil.getValue(name) } - suspend fun findExecutableOnPATH(exe: @NonNls String) = findAllExecutablesOnPATH(exe).firstOrNull() - fun findAllExecutablesOnPATH(exe: @NonNls String) = flow { val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe val paths = path ?: return@flow @@ -55,7 +55,7 @@ data class Env(val env: Map) { continue emit(exePath) } - } + }.flowOn(Dispatchers.IO) companion object { val empty = Env(emptyMap()) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt index 15f9ff71..dfe3ee1f 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt @@ -44,7 +44,10 @@ abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationP it.addItemListener { e -> if (e.stateChange != ItemEvent.SELECTED) return@addItemListener - DirenvService.setStateFor(sharedState, DirenvState.Auto) + val item = e.item + if (item !is DirenvState) + return@addItemListener + DirenvService.setStateFor(sharedState, item) } } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt index 46071cf5..41f85815 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -37,7 +37,8 @@ import javax.swing.JComponent abstract class ZigToolchainConfigurable( val uuid: UUID, tc: T, - val data: ZigProjectConfigurationProvider.IUserDataBridge? + val data: ZigProjectConfigurationProvider.IUserDataBridge?, + val modal: Boolean ): NamedConfigurable() { var toolchain: T = tc set(value) { @@ -46,9 +47,7 @@ abstract class ZigToolchainConfigurable( } init { - data?.putUserData(TOOLCHAIN_KEY, Supplier{ - myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc } - }) + data?.putUserData(TOOLCHAIN_KEY, Supplier{toolchain}) } private var myViews: List> = emptyList() @@ -59,13 +58,14 @@ abstract class ZigToolchainConfigurable( if (views.isEmpty()) { views = ArrayList>() views.add(createPanel()) - views.addAll(createZigToolchainExtensionPanels(data)) - views.forEach { it.reset(toolchain) } + views.addAll(createZigToolchainExtensionPanels(data, if (modal) PanelState.ModalEditor else PanelState.ListEditor)) myViews = views } - return panel { + val p = panel { views.forEach { it.attach(this@panel) } }.withMinimumWidth(20) + views.forEach { it.reset(toolchain) } + return p } override fun getEditableObject(): UUID? { @@ -99,6 +99,6 @@ abstract class ZigToolchainConfigurable( } companion object { - val TOOLCHAIN_KEY: Key> = Key.create("TOOLCHAIN") + val TOOLCHAIN_KEY: Key> = Key.create("TOOLCHAIN") } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt index 8ef0c29f..b9114d00 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt @@ -25,17 +25,22 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.util.UserDataHolder private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainExtensionsProvider") interface ZigToolchainExtensionsProvider { - fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel? + fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): ImmutableElementPanel? val index: Int } -fun createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): List> { +fun createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): List> { return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull { - it.createExtensionPanel(sharedState) + it.createExtensionPanel(sharedState, state) } +} + +enum class PanelState { + ProjectEditor, + ListEditor, + ModalEditor } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt index c9504edc..73c5dc80 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -47,7 +47,7 @@ internal interface ZigToolchainProvider { fun deserialize(data: Map): ZigToolchain? fun serialize(toolchain: ZigToolchain): Map fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean - fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, data: ZigProjectConfigurationProvider.IUserDataBridge?): ZigToolchainConfigurable<*> + fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, data: ZigProjectConfigurationProvider.IUserDataBridge?, modal: Boolean): ZigToolchainConfigurable<*> suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) } @@ -64,9 +64,9 @@ fun ZigToolchain.toRef(): ZigToolchain.Ref { return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this), this.extraData) } -fun ZigToolchain.createNamedConfigurable(uuid: UUID, data: ZigProjectConfigurationProvider.IUserDataBridge?): ZigToolchainConfigurable<*> { +fun ZigToolchain.createNamedConfigurable(uuid: UUID, data: ZigProjectConfigurationProvider.IUserDataBridge?, modal: Boolean): ZigToolchainConfigurable<*> { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return provider.createConfigurable(uuid, this, data) + return provider.createConfigurable(uuid, this, data, modal) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt index c2ab44c9..7df767ef 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt @@ -32,19 +32,10 @@ import java.awt.Component import java.nio.file.Path class LocalToolchainDownloader(component: Component) : Downloader(component) { - override val windowTitle: String get() = ZigBrainsBundle.message("settings.toolchain.downloader.title") - override val versionInfoFetchTitle: @NlsContexts.ProgressTitle String get() = ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch") - override fun downloadProgressTitle(version: ZigVersionInfo): @NlsContexts.ProgressTitle String { - return ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion) - } - override fun localSelector(): LocalSelector { - return LocalToolchainSelector(component) - } - override suspend fun downloadVersionList(): List { - return ZigVersionInfo.downloadVersionList() - } - - override fun getSuggestedPath(): Path? { - return getSuggestedLocalToolchainPath() - } + override val windowTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.title") + override val versionInfoFetchTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch") + override fun downloadProgressTitle(version: ZigVersionInfo) = ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion) + override fun localSelector() = LocalToolchainSelector(component) + override suspend fun downloadVersionList() = ZigVersionInfo.downloadVersionList() + override fun getSuggestedPath() = getSuggestedLocalToolchainPath() } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt index b64b3e26..f570fca4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt @@ -29,8 +29,9 @@ import java.util.UUID class LocalZigToolchainConfigurable( uuid: UUID, toolchain: LocalZigToolchain, - data: ZigProjectConfigurationProvider.IUserDataBridge? -): ZigToolchainConfigurable(uuid, toolchain, data) { + data: ZigProjectConfigurationProvider.IUserDataBridge?, + modal: Boolean +): ZigToolchainConfigurable(uuid, toolchain, data, modal) { override fun createPanel() = LocalZigToolchainPanel() override fun setDisplayName(name: String?) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt index 85736d5e..8b1c2809 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt @@ -100,10 +100,10 @@ class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase { toolchain as LocalZigToolchain - return LocalZigToolchainConfigurable(uuid, toolchain, data) + return LocalZigToolchainConfigurable(uuid, toolchain, data, modal) } @OptIn(ExperimentalCoroutinesApi::class) @@ -114,29 +116,8 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { toolchain as LocalZigToolchain val name = toolchain.name - val path = presentDetectedPath(toolchain.location.pathString) - val primary: String - var secondary: String? - val tooltip: String? - if (isSuggestion) { - primary = path - secondary = name - } else { - primary = name ?: "Zig" - secondary = path - } - if (isSelected) { - tooltip = secondary - secondary = null - } else { - tooltip = null - } - component.append(primary) - if (secondary != null) { - component.append(" ") - component.append(secondary, SimpleTextAttributes.GRAYED_ATTRIBUTES) - } - component.toolTipText = tooltip + val path = toolchain.location.pathString + renderPathNameComponent(path, name, "Zig", component, isSuggestion, isSelected) } } @@ -172,14 +153,4 @@ private fun getWellKnown(): List { } res.add(home.resolve(".zig")) return res -} - -private fun presentDetectedPath(home: String, maxLength: Int = 50, suffixLength: Int = 30): String { - //for macOS, let's try removing Bundle internals - var home = home - home = StringUtil.trimEnd(home, "/Contents/Home") //NON-NLS - home = StringUtil.trimEnd(home, "/Contents/MacOS") //NON-NLS - home = FileUtil.getLocationRelativeToUserHome(home, false) - home = StringUtil.shortenTextWithEllipsis(home, maxLength, suffixLength) - return home } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt index 2737529e..40e62f63 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt @@ -32,5 +32,5 @@ interface ImmutableElementPanel: Disposable { * Returned object must be the exact same class as the provided one. */ fun apply(elem: T): T? - fun reset(elem: T) + fun reset(elem: T?) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt index 275a800c..048b6be5 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt @@ -63,7 +63,7 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { } object ForList: ZigToolchainDriver { - override fun constructModelList(): List> { + override suspend fun constructModelList(): List> { val modelList = ArrayList>() modelList.addAll(ListElem.fetchGroup()) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) @@ -75,12 +75,12 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { uuid: UUID, elem: ZigToolchain ): NamedConfigurable { - return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge()) + return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge(), false) } } class ForSelector(val data: ZigProjectConfigurationProvider.IUserDataBridge): ZigToolchainDriver { - override fun constructModelList(): List> { + override suspend fun constructModelList(): List> { val modelList = ArrayList>() modelList.add(ListElem.None()) modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.instance.name }) @@ -95,7 +95,7 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { uuid: UUID, elem: ZigToolchain ): NamedConfigurable { - return elem.createNamedConfigurable(uuid, data) + return elem.createNamedConfigurable(uuid, data, true) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt index 51e405d4..2ccdb4fe 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -26,7 +26,11 @@ import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.PanelState import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable +import com.falsepattern.zigbrains.project.toolchain.base.createZigToolchainExtensionPanels +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector import com.falsepattern.zigbrains.shared.zigCoroutineScope @@ -35,17 +39,23 @@ import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.Key import com.intellij.ui.dsl.builder.Panel import kotlinx.coroutines.launch +import java.util.UUID +import java.util.function.Supplier class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): UUIDMapSelector(ZigToolchainDriver.ForSelector(sharedState)), SubConfigurable, ZigProjectConfigurationProvider.UserDataListener { + private var myViews: List> = emptyList() init { + sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }}) sharedState.addUserDataChangeListener(this) } override fun onUserDataChanged(key: Key<*>) { + if (key == ZigToolchainConfigurable.TOOLCHAIN_KEY) + return zigCoroutineScope.launch { listChanged() } } @@ -59,24 +69,63 @@ class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvide ) { attachComboBoxRow(this) } + var views = myViews + if (views.isEmpty()) { + views = ArrayList>() + views.addAll(createZigToolchainExtensionPanels(sharedState, PanelState.ProjectEditor)) + myViews = views + } + views.forEach { it.attach(p) } + } + + override fun onSelection(uuid: UUID?) { + sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }}) + refreshViews(uuid) + } + + private fun refreshViews(uuid: UUID?) { + val toolchain = uuid?.let { zigToolchainList[it] } + myViews.forEach { it.reset(toolchain) } } override fun isModified(context: Project): Boolean { - return ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID + val uuid = selectedUUID + if (ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID) { + return true + } + if (uuid == null) + return false + val tc = zigToolchainList[uuid] + if (tc == null) + return false + return myViews.any { it.isModified(tc) } } override fun apply(context: Project) { - ZigToolchainService.getInstance(context).toolchainUUID = selectedUUID + val uuid = selectedUUID + ZigToolchainService.getInstance(context).toolchainUUID = uuid + if (uuid == null) + return + val tc = zigToolchainList[uuid] + if (tc == null) + return + val finalTc = myViews.fold(tc) { acc, view -> view.apply(acc) ?: acc } + zigToolchainList[uuid] = finalTc } override fun reset(context: Project?) { val project = context ?: ProjectManager.getInstance().defaultProject - selectedUUID = ZigToolchainService.getInstance(project).toolchainUUID + val svc = ZigToolchainService.getInstance(project) + val uuid = svc.toolchainUUID + selectedUUID = uuid + refreshViews(uuid) } override fun dispose() { super.dispose() sharedState.removeUserDataChangeListener(this) + myViews.forEach { it.dispose() } + myViews = emptyList() } override val newProjectBeforeInitSelector get() = true diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt index 314754ac..0517f4e3 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt @@ -49,7 +49,7 @@ import javax.swing.event.DocumentEvent import kotlin.io.path.pathString abstract class LocalSelector(val component: Component) { - suspend fun browse(preSelected: Path? = null): T? { + suspend open fun browse(preSelected: Path? = null): T? { return withEDTContext(component.asContextElement()) { doBrowseFromDisk(preSelected) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt index 7a735a6b..d7c639a1 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt @@ -29,7 +29,7 @@ import java.util.UUID interface UUIDComboBoxDriver { val theMap: UUIDMapSerializable.Converting - fun constructModelList(): List> + suspend fun constructModelList(): List> fun createContext(model: ZBModel): ZBContext fun createComboBox(model: ZBModel): ZBComboBox suspend fun resolvePseudo(context: Component, elem: ListElem.Pseudo): UUID? diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt index 75eb7620..69d67c71 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt @@ -25,11 +25,13 @@ 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.launchWithEDT 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.application.ModalityState import com.intellij.openapi.observable.util.whenListChanged import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.ui.MasterDetailsComponent @@ -45,6 +47,7 @@ abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetail private var isTreeInitialized = false private var registered: Boolean = false private var selectOnNextReload: UUID? = null + private var disposed: Boolean = false private val changeListener: StorageChangeListener = { this@UUIDMapEditor.listChanged() } override fun createComponent(): JComponent { @@ -62,14 +65,18 @@ abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetail override fun createActions(fromPopup: Boolean): List { val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.shared.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) { override fun actionPerformed(e: AnActionEvent) { - val modelList = driver.constructModelList() - val model = ZBModel(modelList) - val context = driver.createContext(model) - val popup = ZBComboBoxPopup(context, null, ::onItemSelected) - model.whenListChanged { - popup.syncWithModelChange() + zigCoroutineScope.launchWithEDT(ModalityState.current()) { + if (disposed) + return@launchWithEDT + 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) } - popup.showInBestPositionFor(e.dataContext) } } return listOf(add, MyDeleteAction()) @@ -86,6 +93,8 @@ abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetail if (elem !is ListElem.Pseudo) return zigCoroutineScope.launch(myWholePanel.asContextElement()) { + if (disposed) + return@launch val uuid = driver.resolvePseudo(myWholePanel, elem) if (uuid != null) { withEDTContext(myWholePanel.asContextElement()) { @@ -103,6 +112,7 @@ abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetail override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.shared.list.empty") override fun disposeUIResources() { + disposed = true super.disposeUIResources() if (registered) { driver.theMap.removeChangeListener(changeListener) @@ -115,8 +125,12 @@ abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetail } private fun reloadTree() { + if (disposed) + return val currentSelection = selectedObject?.asSafely() + selectedNode = null myRoot.removeAllChildren() + (myTree.model as DefaultTreeModel).reload() val onReload = selectOnNextReload selectOnNextReload = null var hasOnReload = false @@ -128,12 +142,10 @@ abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetail } (myTree.model as DefaultTreeModel).reload() if (hasOnReload) { - selectNodeInTree(onReload) + selectedNode = findNodeByObject(myRoot, onReload) return } - currentSelection?.let { - selectNodeInTree(it) - } + selectedNode = currentSelection?.let { findNodeByObject(myRoot, it) } } @RequiresEdt @@ -148,6 +160,8 @@ abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetail } private suspend fun listChanged() { + if (disposed) + return withEDTContext(myWholePanel.asContextElement()) { reloadTree() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt index 17a1acd8..2b7486e6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt @@ -32,6 +32,7 @@ import com.falsepattern.zigbrains.shared.ui.ListElem import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.Disposable import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.runInEdt import com.intellij.openapi.observable.util.whenListChanged import com.intellij.openapi.options.ShowSettingsUtil @@ -40,6 +41,7 @@ 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.Runnable import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -54,7 +56,7 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable private var editButton: JButton? = null private val changeListener: StorageChangeListener = { this@UUIDMapSelector.listChanged() } init { - model = ZBModel(driver.constructModelList()) + model = ZBModel(emptyList()) comboBox = driver.createComboBox(model) comboBox.addItemListener(::itemStateChanged) driver.theMap.addChangeListener(changeListener) @@ -67,19 +69,26 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable comboBox.isPopupVisible = true } } + zigCoroutineScope.launchWithEDT(ModalityState.any()) { + model.updateContents(driver.constructModelList()) + } } protected var selectedUUID: UUID? get() = comboBox.selectedUUID set(value) { - runInEdt { + zigCoroutineScope.launchWithEDT(ModalityState.any()) { applyUUIDNowOrOnReload(value) } } + protected open fun onSelection(uuid: UUID?) {} + private fun refreshButtonState(item: ListElem<*>) { - editButton?.isEnabled = item is ListElem.One.Actual<*> + val actual = item is ListElem.One.Actual<*> + editButton?.isEnabled = actual editButton?.repaint() + onSelection(if (actual) item.uuid else null) } private fun itemStateChanged(event: ItemEvent) { @@ -106,6 +115,12 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable @RequiresEdt private fun tryReloadSelection() { val list = model.toList() + if (list.size == 1) { + comboBox.selectedItem = list[0] + comboBox.isEnabled = false + return + } + comboBox.isEnabled = true val onReload = selectOnNextReload selectOnNextReload = null if (onReload != null) { @@ -116,13 +131,13 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable if (element == null) { selectOnNextReload = onReload } else { - model.selectedItem = element + comboBox.selectedItem = element return } } val selected = model.selected if (selected != null && list.contains(selected)) { - model.selectedItem = selected + comboBox.selectedItem = selected return } if (selected is ListElem.One.Actual<*>) { @@ -131,10 +146,10 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable is ListElem.One.Actual -> it.uuid == uuid else -> false } } - model.selectedItem = element + comboBox.selectedItem = element return } - model.selectedItem = ListElem.None() + comboBox.selectedItem = ListElem.None() } protected suspend fun listChanged() { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt index dbb8e82c..ab5812d1 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt @@ -29,11 +29,14 @@ 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.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil 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.concurrency.annotations.RequiresEdt @@ -50,6 +53,7 @@ import java.util.function.Consumer import javax.accessibility.AccessibleContext import javax.swing.JList import javax.swing.border.Border +import kotlin.io.path.pathString class ZBComboBoxPopup( context: ZBContext, @@ -65,7 +69,7 @@ open class ZBComboBox(model: ZBModel, renderer: (() -> ZBModel)-> ZBCel var selectedUUID: UUID? set(value) { if (value == null) { - selectedItem = ListElem.None + selectedItem = ListElem.None() return } for (i in 0..(model: ZBModel, renderer: (() -> ZBModel)-> ZBCel } } } - selectedItem = ListElem.None + selectedItem = ListElem.None() } get() { val item = selectedItem @@ -252,4 +256,40 @@ abstract class ZBCellRenderer(val getModel: () -> ZBModel) : ColoredListCe ) } +fun renderPathNameComponent(path: String, name: String?, nameFallback: String, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { + val path = presentDetectedPath(path) + val primary: String + var secondary: String? + val tooltip: String? + if (isSuggestion) { + primary = path + secondary = name + } else { + primary = name ?: nameFallback + secondary = path + } + if (isSelected) { + tooltip = secondary + secondary = null + } else { + tooltip = null + } + component.append(primary) + if (secondary != null) { + component.append(" ") + component.append(secondary, SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + component.toolTipText = tooltip +} + +fun presentDetectedPath(home: String, maxLength: Int = 50, suffixLength: Int = 30): String { + //for macOS, let's try removing Bundle internals + var home = home + home = StringUtil.trimEnd(home, "/Contents/Home") //NON-NLS + home = StringUtil.trimEnd(home, "/Contents/MacOS") //NON-NLS + home = FileUtil.getLocationRelativeToUserHome(home, false) + home = StringUtil.shortenTextWithEllipsis(home, maxLength, suffixLength) + return home +} + private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/licenses/ZLS.LICENSE b/licenses/ZLS.LICENSE new file mode 100644 index 00000000..2bf33e97 --- /dev/null +++ b/licenses/ZLS.LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) ZLS contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/LSPIcons.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/LSPIcons.kt new file mode 100644 index 00000000..d8647e61 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/LSPIcons.kt @@ -0,0 +1,32 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp + +import com.intellij.openapi.util.IconLoader +import org.jetbrains.annotations.NonNls + +@NonNls +object LSPIcons { + @JvmField + val ZLS = IconLoader.getIcon("/icons/zls.svg", LSPIcons::class.java) +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt new file mode 100644 index 00000000..661e77df --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt @@ -0,0 +1,236 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.settings + +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.config.SemanticTokens +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBCheckBox +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 org.jetbrains.annotations.PropertyKey + +@Suppress("PrivatePropertyName") +class ZLSSettingsPanel() : ImmutableElementPanel { + private val zlsConfigPath = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() + .withTitle(ZLSBundle.message("settings.zls-config-path.browse.title")) + ).also { Disposer.register(this, it) } + 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): Unit = with(p) { + 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) } + collapsibleGroup(ZLSBundle.message("settings.inlay-hints-group.label"), indent = false) { + 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) } + } + + override fun isModified(elem: ZLSSettings): Boolean { + return elem != data + } + + override fun apply(elem: ZLSSettings): ZLSSettings? { + return data + } + + override fun reset(elem: ZLSSettings?) { + data = elem ?: ZLSSettings() + } + + private var data + get() = ZLSSettings( + 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) { + 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 ?: "" + } + + override fun dispose() { + zlsConfigPath.dispose() + } +} + +private fun Panel.fancyRow( + label: @PropertyKey(resourceBundle = "zigbrains.lsp.Bundle") String, + tooltip: @PropertyKey(resourceBundle = "zigbrains.lsp.Bundle") String, + cb: Row.() -> Unit +) = row(ZLSBundle.message(label)) { + contextHelp(ZLSBundle.message(tooltip)) + cb() +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt index b38a3287..e9e67c75 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt @@ -26,6 +26,7 @@ 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.awt.Dimension import java.util.UUID import javax.swing.JComponent @@ -56,9 +57,11 @@ class ZLSConfigurable(val uuid: UUID, zls: ZLSVersion): NamedConfigurable( view.reset(zls) myView = view } - return panel { + val p = panel { view.attach(this@panel) - }.withMaximumWidth(20) + } + p.preferredSize = Dimension(640, 480) + return p } override fun getDisplayName(): @NlsContexts.ConfigurableName String? { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt index 2f93225b..259fbce7 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.lsp.zls +import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsPanel import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase import com.falsepattern.zigbrains.shared.cli.call @@ -58,6 +59,7 @@ class ZLSPanel() : ImmutableNamedElementPanelBase() { Disposer.register(this, it) } private val zlsVersion = JBTextArea().also { it.isEditable = false } + private var settingsPanel: ZLSSettingsPanel? = null private var debounce: Job? = null override fun attach(p: Panel): Unit = with(p) { @@ -68,22 +70,28 @@ class ZLSPanel() : ImmutableNamedElementPanelBase() { row("Version:") { cell(zlsVersion) } + val sp = ZLSSettingsPanel() + p.collapsibleGroup("Settings", indent = false) { + sp.attach(this@collapsibleGroup) + } + settingsPanel = sp } 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 + return name != version.name || version.path != path || settingsPanel?.isModified(version.settings) == true } override fun apply(version: ZLSVersion): ZLSVersion? { val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return null - return version.copy(path = path, name = nameFieldValue ?: "") + return version.copy(path = path, name = nameFieldValue ?: "", settings = settingsPanel?.apply(version.settings) ?: version.settings) } - override fun reset(version: ZLSVersion) { - nameFieldValue = version.name - this.pathToZLS.text = version.path.pathString + override fun reset(version: ZLSVersion?) { + nameFieldValue = version?.name ?: "" + this.pathToZLS.text = version?.path?.pathString ?: "" + settingsPanel?.reset(version?.settings) dispatchUpdateUI() } @@ -127,5 +135,7 @@ class ZLSPanel() : ImmutableNamedElementPanelBase() { override fun dispose() { debounce?.cancel("Disposed") + settingsPanel?.dispose() + settingsPanel = null } } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt index c1de43e8..56ecde3c 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt @@ -63,10 +63,6 @@ data class ZLSVersion(val path: Path, override val name: String? = null, val set companion object { suspend fun tryFromPath(path: Path): ZLSVersion? { - if (path.isDirectory()) { - val exeName = if (SystemInfo.isWindows) "zls.exe" else "zls" - return tryFromPath(path.resolve(exeName)) - } var zls = ZLSVersion(path) if (!zls.isValid()) return null diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt index 97b74ca4..8671d551 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt @@ -23,70 +23,26 @@ package com.falsepattern.zigbrains.lsp.zls.downloader import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.lsp.zls.ui.getSuggestedZLSPath import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.IUserDataBridge import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.shared.downloader.Downloader -import com.falsepattern.zigbrains.shared.downloader.LocalSelector -import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.util.system.OS import java.awt.Component import java.nio.file.Path import kotlin.io.path.isDirectory -class ZLSDownloader(component: Component, private val data: ZigProjectConfigurationProvider.IUserDataBridge?) : Downloader(component) { - override val windowTitle: String - get() = "Install ZLS" - override val versionInfoFetchTitle: @NlsContexts.ProgressTitle String - get() = "Fetching zls version information" - - override fun downloadProgressTitle(version: ZLSVersionInfo): @NlsContexts.ProgressTitle String { - return "Installing ZLS ${version.version.rawVersion}" - } - - override fun localSelector(): LocalSelector { - return ZLSLocalSelector(component) - } - +class ZLSDownloader(component: Component, private val data: IUserDataBridge?) : Downloader(component) { + override val windowTitle get() = "Install ZLS" + override val versionInfoFetchTitle get() = "Fetching zls version information" + override fun downloadProgressTitle(version: ZLSVersionInfo) = "Installing ZLS ${version.version.rawVersion}" + override fun localSelector() = ZLSLocalSelector(component) override suspend fun downloadVersionList(): List { - val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get() ?: return emptyList() - val project = data.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) + val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get() + val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) return ZLSVersionInfo.downloadVersionInfoFor(toolchain, project) } - - override fun getSuggestedPath(): Path? { - return getSuggestedZLSPath() - } -} - -fun getSuggestedZLSPath(): Path? { - return getWellKnownZLS().getOrNull(0) -} - -/** - * Returns the paths to the following list of folders: - * - * 1. DATA/zls - * 2. HOME/.zig - * - * Where DATA is: - * - ~/Library on macOS - * - %LOCALAPPDATA% on Windows - * - $XDG_DATA_HOME (or ~/.local/share if not set) on other OSes - * - * and HOME is the user home path - */ -private fun getWellKnownZLS(): List { - val home = System.getProperty("user.home")?.toNioPathOrNull() ?: return emptyList() - val xdgDataHome = when(OS.CURRENT) { - OS.macOS -> home.resolve("Library") - OS.Windows -> System.getenv("LOCALAPPDATA")?.toNioPathOrNull() - else -> System.getenv("XDG_DATA_HOME")?.toNioPathOrNull() ?: home.resolve(Path.of(".local", "share")) - } - val res = ArrayList() - if (xdgDataHome != null && xdgDataHome.isDirectory()) { - res.add(xdgDataHome.resolve("zls")) - } - res.add(home.resolve(".zls")) - return res + override fun getSuggestedPath() = getSuggestedZLSPath() } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt index 5586bc79..1ef58bad 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt @@ -29,8 +29,10 @@ import com.falsepattern.zigbrains.shared.withUniqueName import com.intellij.icons.AllIcons import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.util.SystemInfo import java.awt.Component import java.nio.file.Path +import kotlin.io.path.isDirectory class ZLSLocalSelector(component: Component) : LocalSelector(component) { override val windowTitle: String @@ -38,6 +40,13 @@ class ZLSLocalSelector(component: Component) : LocalSelector(compone override val descriptor: FileChooserDescriptor get() = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("ZLS binary") + override suspend fun browse(preSelected: Path?): ZLSVersion? { + if (preSelected?.isDirectory() == true) { + return super.browse(preSelected.resolve(if (SystemInfo.isWindows) "zls.exe" else "zls")) + } + return super.browse(preSelected) + } + override suspend fun verify(path: Path): VerifyResult { var zls = resolve(path, null) var result: VerifyResult diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt index 1cc03960..40b942e0 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt @@ -53,12 +53,17 @@ data class ZLSVersionInfo( ): VersionInfo { companion object { @OptIn(ExperimentalSerializationApi::class) - suspend fun downloadVersionInfoFor(toolchain: ZigToolchain, project: Project?): List { + suspend fun downloadVersionInfoFor(toolchain: ZigToolchain?, project: Project?): List { return withContext(Dispatchers.IO) { - val zigVersion = toolchain.zig.getEnv(project).getOrNull()?.version ?: return@withContext emptyList() + val single = toolchain != null + val url = if (single) { + getToolchainURL(toolchain, project) ?: return@withContext emptyList() + } else { + multiURL + } val service = DownloadableFileService.getInstance() val tempFile = FileUtil.createTempFile(tempPluginDir, "zls_version_info", ".json", false, false) - val desc = service.createFileDescription("https://releases.zigtools.org/v1/zls/select-version?zig_version=${URLEncoder.encode(zigVersion, Charsets.UTF_8)}&compatibility=only-runtime", tempFile.name) + val desc = service.createFileDescription(url, tempFile.name) val downloader = service.createDownloader(listOf(desc), "ZLS version information") val downloadResults = coroutineToIndicator { downloader.download(tempPluginDir) @@ -68,13 +73,27 @@ data class ZLSVersionInfo( val index = downloadResults[0].first val info = index.inputStream().use { Json.decodeFromStream(it) } index.delete() - return@withContext listOfNotNull(parseVersion(info)) + return@withContext if (single) { + listOfNotNull(parseVersion(null, info)) + } else { + info.mapNotNull { (key, value) -> parseVersion(key, value) } + } } } } } -private fun parseVersion(data: JsonObject): ZLSVersionInfo? { - val versionTag = data["version"]?.asSafely()?.content + +private suspend fun getToolchainURL(toolchain: ZigToolchain, project: Project?): String? { + val zigVersion = toolchain.zig.getEnv(project).getOrNull()?.version ?: return null + return "https://releases.zigtools.org/v1/zls/select-version?zig_version=${URLEncoder.encode(zigVersion, Charsets.UTF_8)}&compatibility=only-runtime" +} +private const val multiURL: String = "https://builds.zigtools.org/index.json" +private fun parseVersion(versionKey: String?, data: JsonElement): ZLSVersionInfo? { + if (data !is JsonObject) { + return null + } + + val versionTag = data["version"]?.asSafely()?.content ?: versionKey val version = SemVer.parseFromText(versionTag) ?: return null val date = data["date"]?.asSafely()?.content ?: "" diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt index e64d3c7a..6b7133e4 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt @@ -22,13 +22,16 @@ package com.falsepattern.zigbrains.lsp.zls.ui +import com.falsepattern.zigbrains.direnv.DirenvService +import com.falsepattern.zigbrains.direnv.Env +import com.falsepattern.zigbrains.lsp.ZLSBundle import com.falsepattern.zigbrains.lsp.zls.ZLSConfigurable import com.falsepattern.zigbrains.lsp.zls.ZLSVersion import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSDownloader import com.falsepattern.zigbrains.lsp.zls.downloader.ZLSLocalSelector import com.falsepattern.zigbrains.lsp.zls.zlsInstallations import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable.Companion.TOOLCHAIN_KEY import com.falsepattern.zigbrains.shared.UUIDMapSerializable import com.falsepattern.zigbrains.shared.ui.ListElem import com.falsepattern.zigbrains.shared.ui.ListElem.One.Actual @@ -39,15 +42,25 @@ 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.asPending -import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.falsepattern.zigbrains.shared.withUniqueName +import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.util.system.OS import com.intellij.util.text.SemVer -import kotlinx.coroutines.async +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.flowOn import java.awt.Component +import java.nio.file.Files +import java.nio.file.Path import java.util.UUID +import kotlin.io.path.isDirectory +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile sealed interface ZLSDriver: UUIDComboBoxDriver { override val theMap: UUIDMapSerializable.Converting @@ -70,58 +83,146 @@ sealed interface ZLSDriver: UUIDComboBoxDriver { elem: ListElem.Pseudo ): UUID? { return when(elem) { - is ListElem.FromDisk<*> -> ZLSLocalSelector(context).browse() - else -> null + is ListElem.One.Suggested -> zlsInstallations.withUniqueName(elem.instance) + is ListElem.FromDisk -> ZLSLocalSelector(context).browse() + is ListElem.Download -> ZLSDownloader(context, data).download() }?.let { zlsInstallations.registerNew(it) } } - object ForList: ZLSDriver { - override fun constructModelList(): List> { - return listOf(ListElem.None(), Separator("", true), ListElem.FromDisk()) - } - } + val data: ZigProjectConfigurationProvider.IUserDataBridge? - @JvmRecord - data class ForSelector(private val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver { - override fun constructModelList(): List> { + object ForList: ZLSDriver { + override suspend fun constructModelList(): List> { val res = ArrayList>() - res.add(ListElem.None()) - res.add(compatibleInstallations().asPending()) - res.add(Separator("", true)) res.addAll(ListElem.fetchGroup()) + res.add(Separator(ZLSBundle.message("settings.model.detected.separator"), true)) + res.add(suggestZLSVersions().asPending()) return res } - override suspend fun resolvePseudo( - context: Component, - elem: ListElem.Pseudo - ): UUID? { - return when(elem) { - is ListElem.FromDisk<*> -> ZLSLocalSelector(context).browse() - is ListElem.Download<*> -> ZLSDownloader(context, data).download() - else -> null - }?.let { zlsInstallations.registerNew(it) } - } - private fun compatibleInstallations(): Flow> = flow { - val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) - val toolchainVersion = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY) - ?.get() - ?.zig - ?.getEnv(project) - ?.getOrNull() - ?.version - ?.let { SemVer.parseFromText(it) } - ?: return@flow - zlsInstallations.forEach { (uuid, version) -> - val zlsVersion = version.version() ?: return@forEach - if (numericVersionEquals(toolchainVersion, zlsVersion)) { - emit(Actual(uuid, version)) - } + override val data: ZigProjectConfigurationProvider.IUserDataBridge? + get() = null + } + + @JvmRecord + data class ForSelector(override val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver { + override suspend fun constructModelList(): List> { + val (project, toolchainVersion) = unpack(data) + if (toolchainVersion == null) { + return listOf(ListElem.None()) } + val res = ArrayList>() + res.add(ListElem.None()) + res.addAll(compatibleInstallations(toolchainVersion)) + res.add(Separator("", true)) + res.addAll(ListElem.fetchGroup()) + res.add(Separator(ZLSBundle.message("settings.model.detected.separator"), true)) + res.add(suggestZLSVersions(project, data, toolchainVersion).asPending()) + return res } } } +private suspend fun unpack(data: ZigProjectConfigurationProvider.IUserDataBridge?): Pair { + val toolchain = data?.getUserData(TOOLCHAIN_KEY)?.get() + val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY) + val toolchainVersion = toolchain + ?.zig + ?.getEnv(project) + ?.getOrNull() + ?.version + ?.let { SemVer.parseFromText(it) } + return project to toolchainVersion +} + +private fun suggestZLSVersions(project: Project? = null, data: ZigProjectConfigurationProvider.IUserDataBridge? = null, toolchainVersion: SemVer? = null): Flow = flow { + val env = if (project != null && DirenvService.getStateFor(data, project).isEnabled(project)) { + DirenvService.getInstance(project).import() + } else { + Env.empty + } + val existing = zlsInstallations.map { (_, zls) -> zls } + env.findAllExecutablesOnPATH("zls").collect { path -> + if (existing.any { it.path == path }) { + return@collect + } + emitIfCompatible(path, toolchainVersion) + } + val exe = if (SystemInfo.isWindows) "zls.exe" else "zls" + getWellKnownZLS().forEach { wellKnown -> + runCatching { + Files.newDirectoryStream(wellKnown).use { stream -> + stream.asSequence().filterNotNull().forEach { dir -> + val path = dir.resolve(exe) + if (!path.isRegularFile() || !path.isExecutable()) { + return@forEach + } + if (existing.any { it.path == path }) { + return@forEach + } + emitIfCompatible(path, toolchainVersion) + } + } + } + } +}.flowOn(Dispatchers.IO) + +private suspend fun FlowCollector.emitIfCompatible(path: Path, toolchainVersion: SemVer?) { + val ver = ZLSVersion.tryFromPath(path) ?: return + if (isCompatible(ver, toolchainVersion)) { + emit(ver) + } +} + +private suspend fun compatibleInstallations(toolchainVersion: SemVer): List> { + return zlsInstallations.mapNotNull { (uuid, version) -> + if (!isCompatible(version, toolchainVersion)) { + return@mapNotNull null + } + Actual(uuid, version) + } +} + +private suspend fun isCompatible(version: ZLSVersion, toolchainVersion: SemVer?): Boolean { + if (toolchainVersion == null) + return true + val zlsVersion = version.version() ?: return false + return numericVersionEquals(zlsVersion, toolchainVersion) +} + private fun numericVersionEquals(a: SemVer, b: SemVer): Boolean { return a.major == b.major && a.minor == b.minor && a.patch == b.patch +} + + +fun getSuggestedZLSPath(): Path? { + return getWellKnownZLS().getOrNull(0) +} + +/** + * Returns the paths to the following list of folders: + * + * 1. DATA/zls + * 2. HOME/.zig + * + * Where DATA is: + * - ~/Library on macOS + * - %LOCALAPPDATA% on Windows + * - $XDG_DATA_HOME (or ~/.local/share if not set) on other OSes + * + * and HOME is the user home path + */ +private fun getWellKnownZLS(): List { + val home = System.getProperty("user.home")?.toNioPathOrNull() ?: return emptyList() + val xdgDataHome = when(OS.CURRENT) { + OS.macOS -> home.resolve("Library") + OS.Windows -> System.getenv("LOCALAPPDATA")?.toNioPathOrNull() + else -> System.getenv("XDG_DATA_HOME")?.toNioPathOrNull() ?: home.resolve(Path.of(".local", "share")) + } + val res = ArrayList() + if (xdgDataHome != null && xdgDataHome.isDirectory()) { + res.add(xdgDataHome.resolve("zls")) + } + res.add(home.resolve(".zls")) + return res } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt index f80d888d..a69b29da 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt @@ -28,6 +28,7 @@ import com.falsepattern.zigbrains.lsp.zls.ZLSVersion import com.falsepattern.zigbrains.lsp.zls.withZLS import com.falsepattern.zigbrains.lsp.zls.zlsUUID import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.toolchain.base.PanelState import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainExtensionsProvider import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel @@ -51,7 +52,7 @@ class ZLSEditor(private val sharedState: ZigProjectConfiguratio } override fun attach(panel: Panel): Unit = with(panel) { - row("ZLS") { + row("Language Server") { attachComboBoxRow(this) } } @@ -64,8 +65,12 @@ class ZLSEditor(private val sharedState: ZigProjectConfiguratio return toolchain.withZLS(selectedUUID) } - override fun reset(toolchain: T) { - selectedUUID = toolchain.zlsUUID + override fun reset(toolchain: T?) { + selectedUUID = toolchain?.zlsUUID + zigCoroutineScope.launch { + listChanged() + selectedUUID = toolchain?.zlsUUID + } } override fun dispose() { @@ -74,7 +79,10 @@ class ZLSEditor(private val sharedState: ZigProjectConfiguratio } class Provider: ZigToolchainExtensionsProvider { - override fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel? { + override fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): ImmutableElementPanel? { + if (state == PanelState.ModalEditor) { + return null + } return ZLSEditor(sharedState) } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt index 9469478c..8b67cff0 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt @@ -25,7 +25,6 @@ package com.falsepattern.zigbrains.lsp.zls.ui import com.falsepattern.zigbrains.lsp.ZLSBundle import com.falsepattern.zigbrains.lsp.zls.ZLSVersion import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor -import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.openapi.util.NlsContexts class ZLSListEditor : UUIDMapEditor(ZLSDriver.ForList) { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt index cde2f702..53c680d5 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt @@ -22,15 +22,15 @@ package com.falsepattern.zigbrains.lsp.zls.ui -import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.lsp.LSPIcons 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.falsepattern.zigbrains.shared.ui.renderPathNameComponent import com.intellij.icons.AllIcons import com.intellij.openapi.project.Project import com.intellij.ui.SimpleTextAttributes @@ -56,17 +56,13 @@ class ZLSCellRenderer(getModel: () -> ZBModel): ZBCellRenderer { val (icon, isSuggestion) = when(value) { is ListElem.One.Suggested -> AllIcons.General.Information to true - is ListElem.One.Actual -> Icons.Zig to false + is ListElem.One.Actual -> LSPIcons.ZLS 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) - } + val name = item.name + val path = item.path.pathString + renderPathNameComponent(path, name, "ZLS", this, isSuggestion, index == -1) } is ListElem.Download -> { diff --git a/lsp/src/main/resources/icons/zls.svg b/lsp/src/main/resources/icons/zls.svg new file mode 100644 index 00000000..5fbf01fb --- /dev/null +++ b/lsp/src/main/resources/icons/zls.svg @@ -0,0 +1,19 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties index 1696dace..2ab4818a 100644 --- a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties +++ b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties @@ -68,7 +68,7 @@ lsp.zls.name=Zig Language Server lsp.zls.description=The Zig Language Server, via ZigBrains settings.list.title=ZLS Instances settings.list.empty=Select a ZLS version to view or edit its details here -settings.model.detected.separator=Detected ZLS version +settings.model.detected.separator=Detected ZLS versions settings.model.none.text= settings.model.loading.text=Loading\u2026 settings.model.from-disk.text=Add ZLS from disk\u2026 diff --git a/src/art/zls/zls.svg b/src/art/zls/zls.svg new file mode 100644 index 00000000..893dff1f --- /dev/null +++ b/src/art/zls/zls.svg @@ -0,0 +1,19 @@ + + + + + + image/svg+xml + + + + + + + + + + + + +