From f53b0e3283cb998c8a4d5d44aa1da80a085cd2b4 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Sat, 5 Apr 2025 13:33:31 +0200 Subject: [PATCH 01/22] begin implementation of multi-toolchain management --- .../project/toolchain/AbstractZigToolchain.kt | 1 + .../project/toolchain/LocalZigToolchain.kt | 15 +- .../LocalZigToolchainConfigurable.kt | 89 +++++++++ .../toolchain/LocalZigToolchainPanel.kt | 176 ++++++++++++++++++ .../zigbrains/project/toolchain/ZigSDKType.kt | 92 +++++++++ .../project/toolchain/ZigToolchainList.kt | 30 +++ .../toolchain/ZigToolchainListEditor.kt | 99 ++++++++++ .../toolchain/ZigToolchainListService.kt | 58 ++++++ .../project/toolchain/ZigToolchainProvider.kt | 41 +++- .../project/toolchain/tools/ZigTool.kt | 6 + .../resources/META-INF/zigbrains-core.xml | 7 + .../resources/zigbrains/Bundle.properties | 2 + 12 files changed, 606 insertions(+), 10 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigSDKType.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainList.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt index ec3e3b94..8db941ba 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt @@ -27,6 +27,7 @@ import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project import java.nio.file.Path + abstract class AbstractZigToolchain { val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt index b534ecb7..76e6d8cb 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt @@ -30,10 +30,11 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull import java.nio.file.Path -class LocalZigToolchain(val location: Path): AbstractZigToolchain() { +class LocalZigToolchain(var location: Path, var std: Path? = null, var name: String? = null): AbstractZigToolchain() { override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } @@ -62,5 +63,17 @@ class LocalZigToolchain(val location: Path): AbstractZigToolchain() { throw ExecutionException("The debugger only supports local zig toolchain") } } + + fun tryFromPathString(pathStr: String): LocalZigToolchain? { + return pathStr.toNioPathOrNull()?.let(::tryFromPath) + } + + fun tryFromPath(path: Path): LocalZigToolchain? { + val tc = LocalZigToolchain(path) + if (!tc.zig.fileValid()) { + return null + } + return tc + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt new file mode 100644 index 00000000..95ea83c9 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt @@ -0,0 +1,89 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.intellij.openapi.project.Project +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 kotlinx.coroutines.runBlocking +import javax.swing.JComponent + +class LocalZigToolchainConfigurable( + val toolchain: LocalZigToolchain, + private val project: Project +): NamedConfigurable() { + private var myView: LocalZigToolchainPanel? = null + override fun setDisplayName(name: @NlsSafe String?) { + toolchain.name = name + } + + override fun getEditableObject(): LocalZigToolchain? { + return toolchain + } + + override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { + return displayName + } + + override fun createOptionsPanel(): JComponent? { + var view = myView + if (view == null) { + view = LocalZigToolchainPanel() + view.reset(this) + myView = view + } + return panel { + view.attach(this) + } + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + var theName = toolchain.name + if (theName == null) { + val version = toolchain.zig.let { runBlocking { it.getEnv(project) } }.getOrNull()?.version + if (version != null) { + theName = "Zig $version" + toolchain.name = theName + } + } + return theName + } + + override fun isModified(): Boolean { + return myView?.isModified(this) == true + } + + override fun apply() { + myView?.apply(this) + } + + override fun reset() { + myView?.reset(this) + } + + override fun disposeUIResources() { + super.disposeUIResources() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt new file mode 100644 index 00000000..e35d8e7e --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt @@ -0,0 +1,176 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.Disposable +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.JBCheckBox +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.JBTextField +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 LocalZigToolchainPanel() : Disposable { + private val nameField = JBTextField() + private val pathToToolchain = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + ).also { + it.textField.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + dispatchUpdateUI() + } + }) + Disposer.register(this, it) + } + private val toolchainVersion = JBTextArea().also { it.isEditable = false } + private val stdFieldOverride = JBCheckBox(ZigBrainsBundle.message("settings.project.label.override-std")).apply { + addChangeListener { + if (isSelected) { + pathToStd.isEnabled = true + } else { + pathToStd.isEnabled = false + updateUI() + } + } + } + private val pathToStd = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-std")) + ).also { Disposer.register(this, it) } + private var debounce: Job? = null + + fun attach(p: Panel): Unit = with(p) { + row("Name") { + cell(nameField).resizableColumn().align(AlignX.FILL) + } + separator() + row(ZigBrainsBundle.message("settings.project.label.toolchain")) { + cell(pathToToolchain).resizableColumn().align(AlignX.FILL) + } + row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) { + cell(toolchainVersion) + } + row(ZigBrainsBundle.message("settings.project.label.std-location")) { + cell(pathToStd).resizableColumn().align(AlignX.FILL) + cell(stdFieldOverride) + } + } + + fun isModified(cfg: LocalZigToolchainConfigurable): Boolean { + val name = nameField.text.ifBlank { null } ?: return false + val tc = cfg.toolchain + val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false + val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null + return name != cfg.displayName || tc.location != location || tc.std != std + } + + fun apply(cfg: LocalZigToolchainConfigurable): Boolean { + cfg.displayName = nameField.text + val tc = cfg.toolchain + val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false + val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null + tc.location = location + tc.std = std + return true + } + + fun reset(cfg: LocalZigToolchainConfigurable) { + nameField.text = cfg.displayName ?: "" + val tc = cfg.toolchain + this.pathToToolchain.text = tc.location.pathString + val std = tc.std + if (std != null) { + stdFieldOverride.isSelected = true + pathToStd.text = std.pathString + pathToStd.isEnabled = true + } else { + stdFieldOverride.isSelected = false + pathToStd.text = "" + pathToStd.isEnabled = false + dispatchUpdateUI() + } + } + + private fun dispatchUpdateUI() { + debounce?.cancel("New debounce") + debounce = zigCoroutineScope.launch { + updateUI() + } + } + + private suspend fun updateUI() { + delay(200) + val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() + if (pathToToolchain == null) { + withEDTContext(ModalityState.any()) { + toolchainVersion.text = "[toolchain path empty or invalid]" + if (!stdFieldOverride.isSelected) { + pathToStd.text = "" + } + } + return + } + val toolchain = LocalZigToolchain(pathToToolchain) + val zig = toolchain.zig + val env = zig.getEnv(null).getOrElse { throwable -> + throwable.printStackTrace() + withEDTContext(ModalityState.any()) { + toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}" + if (!stdFieldOverride.isSelected) { + pathToStd.text = "" + } + } + return + } + val version = env.version + val stdPath = env.stdPath(toolchain, null) + + withEDTContext(ModalityState.any()) { + toolchainVersion.text = version + toolchainVersion.foreground = JBColor.foreground() + if (!stdFieldOverride.isSelected) { + pathToStd.text = stdPath?.pathString ?: "" + } + } + } + + override fun dispose() { + debounce?.cancel("Disposed") + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigSDKType.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigSDKType.kt new file mode 100644 index 00000000..31f25638 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigSDKType.kt @@ -0,0 +1,92 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.falsepattern.zigbrains.direnv.emptyEnv +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.AdditionalDataConfigurable +import com.intellij.openapi.projectRoots.SdkAdditionalData +import com.intellij.openapi.projectRoots.SdkModel +import com.intellij.openapi.projectRoots.SdkModificator +import com.intellij.openapi.projectRoots.SdkType +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.EnvironmentUtil +import com.intellij.util.asSafely +import com.intellij.util.system.OS +import kotlinx.coroutines.runBlocking +import org.jdom.Element +import org.jetbrains.annotations.Nls +import java.io.File +import kotlin.io.path.pathString + +class ZigSDKType: SdkType("Zig") { + override fun suggestHomePath(): String? { + return null + } + + private fun getPathEnv(path: String): ZigToolchainEnvironmentSerializable? { + return LocalZigToolchain.tryFromPathString(path)?.zig?.let { runBlocking { it.getEnv(null) } }?.getOrNull() + } + + override fun isValidSdkHome(path: String): Boolean { + return LocalZigToolchain.tryFromPathString(path) != null + } + + override fun suggestSdkName(currentSdkName: String?, sdkHome: String): String { + return getVersionString(sdkHome)?.let { "Zig $it" } ?: currentSdkName ?: "Zig" + } + + override fun getVersionString(sdkHome: String): String? { + return getPathEnv(sdkHome)?.version + } + + override fun suggestHomePaths(): Collection { + val res = HashSet() + EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } + if (OS.CURRENT != OS.Windows) { + EnvironmentUtil.getValue("HOME")?.let { res.add("$it/.local/share/zigup") } + EnvironmentUtil.getValue("XDG_DATA_HOME")?.let { res.add("$it/zigup") } + } + return res + } + + override fun createAdditionalDataConfigurable( + sdkModel: SdkModel, + sdkModificator: SdkModificator + ): AdditionalDataConfigurable? { + return null + } + + override fun getPresentableName(): @Nls(capitalization = Nls.Capitalization.Title) String { + return "Zig" + } + + override fun saveAdditionalData(additionalData: SdkAdditionalData, additional: Element) { + + } + + override fun isRelevantForFile(project: Project, file: VirtualFile): Boolean { + return file.extension == "zig" || file.extension == "zon" + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainList.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainList.kt new file mode 100644 index 00000000..62fac96a --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainList.kt @@ -0,0 +1,30 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.intellij.util.xmlb.annotations.OptionTag + +data class ZigToolchainList( + @get:OptionTag(converter = ZigToolchainListConverter::class) + var toolchains: List = emptyList(), +) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt new file mode 100644 index 00000000..6ff47dcd --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt @@ -0,0 +1,99 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.roots.ui.configuration.SdkPopupFactory +import com.intellij.openapi.ui.MasterDetailsComponent +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.util.IconUtil +import javax.swing.JComponent +import javax.swing.tree.DefaultTreeModel + +class ZigToolchainListEditor(): MasterDetailsComponent() { + private var isTreeInitialized = false + + override fun createComponent(): JComponent { + if (!isTreeInitialized) { + initTree() + isTreeInitialized = true + } + return super.createComponent() + } + + override fun createActions(fromPopup: Boolean): List? { + val add = object : DumbAwareAction({"lmaoo"}, Presentation.NULL_STRING, IconUtil.addIcon) { + override fun actionPerformed(e: AnActionEvent) { + SdkPopupFactory + .newBuilder() + .withSdkTypeFilter { it is ZigSDKType } + .onSdkSelected { + val path = it.homePath?.toNioPathOrNull() ?: return@onSdkSelected + val toolchain = LocalZigToolchain(path) + zigToolchainList.state = ZigToolchainList(zigToolchainList.state.toolchains + listOf(toolchain)) + addLocalToolchain(toolchain) + (myTree.model as DefaultTreeModel).reload() + } + .buildPopup() + .showPopup(e) + } + } + return listOf(add, MyDeleteAction()) + } + + override fun onItemDeleted(item: Any?) { + if (item is AbstractZigToolchain) { + zigToolchainList.state = ZigToolchainList(zigToolchainList.state.toolchains.filter { it != item }) + } + super.onItemDeleted(item) + } + + override fun reset() { + reloadTree() + super.reset() + } + + override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchains.empty") + + override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") + + private fun addLocalToolchain(toolchain: LocalZigToolchain) { + val node = MyNode(LocalZigToolchainConfigurable(toolchain, ProjectManager.getInstance().defaultProject)) + addNode(node, myRoot) + } + + private fun reloadTree() { + myRoot.removeAllChildren() + zigToolchainList.state.toolchains.forEach { toolchain -> + if (toolchain is LocalZigToolchain) { + addLocalToolchain(toolchain) + } + } + (myTree.model as DefaultTreeModel).reload() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt new file mode 100644 index 00000000..5c301b90 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -0,0 +1,58 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.falsepattern.zigbrains.project.settings.ZigProjectSettings +import com.falsepattern.zigbrains.project.toolchain.stdlib.ZigSyntheticLibrary +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.components.* +import com.intellij.openapi.project.Project +import kotlinx.coroutines.launch + +@Service(Service.Level.APP) +@State( + name = "ZigProjectSettings", + storages = [Storage("zigbrains.xml")] +) +class ZigToolchainListService(): PersistentStateComponent { + @Volatile + private var state = ZigToolchainList() + + override fun getState(): ZigToolchainList { + return state.copy() + } + + fun setState(value: ZigToolchainList) { + this.state = value + } + + override fun loadState(state: ZigToolchainList) { + setState(state) + } + + fun isModified(otherData: ZigToolchainList): Boolean { + return state != otherData + } +} + +val zigToolchainList get() = service() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt index 57aeb51b..1ad68e8d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider.Compani import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.util.UserDataHolder +import com.intellij.util.asSafely import com.intellij.util.xmlb.Converter import kotlinx.serialization.json.* @@ -43,6 +44,21 @@ sealed interface ZigToolchainProvider { suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain? { return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(project, extraData) } } + + fun fromJson(json: JsonObject): AbstractZigToolchain? { + val marker = (json["marker"] as? JsonPrimitive)?.contentOrNull ?: return null + val data = json["data"] ?: return null + val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null + return provider.deserialize(data) + } + + fun toJson(tc: AbstractZigToolchain): JsonObject? { + val provider = EXTENSION_POINT_NAME.extensionList.find { it.canSerialize(tc) } ?: return null + return buildJsonObject { + put("marker", provider.serialMarker) + put("data", provider.serialize(tc)) + } + } } } @@ -52,18 +68,25 @@ private fun ZigToolchainProvider.serialize(toolchai class ZigToolchainConverter: Converter() { override fun fromString(value: String): AbstractZigToolchain? { val json = Json.parseToJsonElement(value) as? JsonObject ?: return null - val marker = (json["marker"] as? JsonPrimitive)?.contentOrNull ?: return null - val data = json["data"] ?: return null - val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null - return provider.deserialize(data) + return ZigToolchainProvider.fromJson(json) } override fun toString(value: AbstractZigToolchain): String? { - val provider = EXTENSION_POINT_NAME.extensionList.find { it.canSerialize(value) } ?: return null - return buildJsonObject { - put("marker", provider.serialMarker) - put("data", provider.serialize(value)) - }.toString() + return ZigToolchainProvider.toJson(value)?.toString() + } +} + +class ZigToolchainListConverter: Converter>() { + override fun fromString(value: String): List { + val json = Json.parseToJsonElement(value) as? JsonArray ?: return emptyList() + return json.mapNotNull { it.asSafely()?.let { ZigToolchainProvider.fromJson(it) } } } + override fun toString(value: List): String { + return buildJsonArray { + value.mapNotNull { ZigToolchainProvider.toJson(it) }.forEach { + add(it) + } + }.toString() + } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt index 27b106bf..c7a1e507 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt @@ -29,6 +29,7 @@ import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.ProcessOutput import com.intellij.openapi.project.Project import java.nio.file.Path +import kotlin.io.path.isRegularFile abstract class ZigTool(val toolchain: AbstractZigToolchain) { abstract val toolName: String @@ -38,6 +39,11 @@ abstract class ZigTool(val toolchain: AbstractZigToolchain) { return cli.call(timeoutMillis, ipcProject = ipcProject) } + fun fileValid(): Boolean { + val exe = toolchain.pathToExecutable(toolName) + return exe.isRegularFile() + } + private suspend fun createBaseCommandLine( workingDirectory: Path?, vararg parameters: String diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index 45220600..ec6a4f81 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -144,6 +144,13 @@ id="ZigConfigurable" displayName="Zig" /> + + Date: Sat, 5 Apr 2025 19:46:48 +0200 Subject: [PATCH 02/22] more progress --- .../com/falsepattern/zigbrains/ZBStartup.kt | 3 +- .../settings/ZigProjectSettingsPanel.kt | 3 +- .../project/toolchain/AbstractZigToolchain.kt | 14 +++ .../project/toolchain/LocalZigToolchain.kt | 4 +- .../LocalZigToolchainConfigurable.kt | 22 ++-- .../toolchain/LocalZigToolchainPanel.kt | 4 +- .../toolchain/LocalZigToolchainProvider.kt | 61 +++++++--- .../zigbrains/project/toolchain/ZigSDKType.kt | 92 --------------- .../project/toolchain/ZigToolchainList.kt | 30 ----- .../toolchain/ZigToolchainListEditor.kt | 109 ++++++++++++++---- .../toolchain/ZigToolchainListService.kt | 51 +++++--- .../project/toolchain/ZigToolchainProvider.kt | 94 +++++++-------- .../resources/META-INF/zigbrains-core.xml | 1 - .../toolchain/ToolchainZLSConfigProvider.kt | 2 +- 14 files changed, 246 insertions(+), 244 deletions(-) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigSDKType.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainList.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt index b51e5b7f..fa4d6f56 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider +import com.falsepattern.zigbrains.project.toolchain.suggestZigToolchain import com.intellij.ide.BrowserUtil import com.intellij.ide.plugins.PluginManager import com.intellij.notification.Notification @@ -80,7 +81,7 @@ class ZBStartup: ProjectActivity { data.putUserData(LocalZigToolchain.DIRENV_KEY, DirenvCmd.direnvInstalled() && !project.isDefault && zigProjectState.direnv ) - val tc = ZigToolchainProvider.suggestToolchain(project, data) ?: return + val tc = project.suggestZigToolchain(data) ?: return if (tc is LocalZigToolchain) { zigProjectState.toolchainPath = tc.location.pathString project.zigProjectSettings.state = zigProjectState diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt index 9c998089..b9a388a1 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider +import com.falsepattern.zigbrains.project.toolchain.suggestZigToolchain import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope @@ -107,7 +108,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide return val data = UserDataHolderBase() data.putUserData(LocalZigToolchain.DIRENV_KEY, !project.isDefault && direnv.isSelected && DirenvCmd.direnvInstalled()) - val tc = ZigToolchainProvider.suggestToolchain(project, data) ?: return + val tc = project.suggestZigToolchain(project) ?: return if (tc !is LocalZigToolchain) { TODO("Implement non-local zig toolchain in config") } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt index 8db941ba..a388e64d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt @@ -25,7 +25,12 @@ package com.falsepattern.zigbrains.project.toolchain import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.Converter +import com.intellij.util.xmlb.annotations.Attribute +import com.intellij.util.xmlb.annotations.MapAnnotation +import com.intellij.util.xmlb.annotations.OptionTag import java.nio.file.Path +import java.util.UUID abstract class AbstractZigToolchain { @@ -36,4 +41,13 @@ abstract class AbstractZigToolchain { abstract suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine abstract fun pathToExecutable(toolName: String, project: Project? = null): Path + + data class Ref( + @JvmField + @Attribute + val marker: String? = null, + @JvmField + @MapAnnotation(surroundWithTag = false) + val data: Map? = null, + ) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt index 76e6d8cb..2eeca5fe 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt @@ -33,8 +33,10 @@ import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull import java.nio.file.Path +import java.util.UUID +import kotlin.io.path.pathString -class LocalZigToolchain(var location: Path, var std: Path? = null, var name: String? = null): AbstractZigToolchain() { +data class LocalZigToolchain(val location: Path, val std: Path? = null, val name: String? = null): AbstractZigToolchain() { override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt index 95ea83c9..3cfb420d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt @@ -25,22 +25,28 @@ package com.falsepattern.zigbrains.project.toolchain import com.intellij.openapi.project.Project 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 kotlinx.coroutines.runBlocking +import java.util.UUID import javax.swing.JComponent class LocalZigToolchainConfigurable( - val toolchain: LocalZigToolchain, + val uuid: UUID, + toolchain: LocalZigToolchain, private val project: Project -): NamedConfigurable() { +): NamedConfigurable() { + var toolchain: LocalZigToolchain = toolchain + set(value) { + zigToolchainList.setToolchain(uuid, value) + field = value + } private var myView: LocalZigToolchainPanel? = null - override fun setDisplayName(name: @NlsSafe String?) { - toolchain.name = name + override fun setDisplayName(name: String?) { + toolchain = toolchain.copy(name = name) } - override fun getEditableObject(): LocalZigToolchain? { - return toolchain + override fun getEditableObject(): UUID { + return uuid } override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { @@ -65,7 +71,7 @@ class LocalZigToolchainConfigurable( val version = toolchain.zig.let { runBlocking { it.getEnv(project) } }.getOrNull()?.version if (version != null) { theName = "Zig $version" - toolchain.name = theName + toolchain = toolchain.copy(name = theName) } } return theName diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt index e35d8e7e..4cea024c 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt @@ -101,12 +101,10 @@ class LocalZigToolchainPanel() : Disposable { } fun apply(cfg: LocalZigToolchainConfigurable): Boolean { - cfg.displayName = nameField.text val tc = cfg.toolchain val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null - tc.location = location - tc.std = std + cfg.toolchain = tc.copy(location = location, std = std, name = nameField.text ?: "") return true } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt index a7701ad6..d49f652b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt @@ -26,12 +26,22 @@ import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.direnv.emptyEnv import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.openapi.roots.ui.configuration.SdkPopupBuilder +import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.util.EnvironmentUtil +import com.intellij.util.system.OS +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.mapNotNull import kotlinx.serialization.json.* +import java.io.File +import java.util.UUID import kotlin.io.path.pathString -class LocalZigToolchainProvider: ZigToolchainProvider { +class LocalZigToolchainProvider: ZigToolchainProvider { override suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): LocalZigToolchain? { val env = if (project != null && (extraData.getUserData(LocalZigToolchain.DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { DirenvCmd.importDirenv(project) @@ -45,22 +55,47 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override val serialMarker: String get() = "local" - override fun deserialize(data: JsonElement): LocalZigToolchain? { - if (data !is JsonObject) - return null - - val loc = data["location"] as? JsonPrimitive ?: return null - val path = loc.content.toNioPathOrNull() ?: return null - return LocalZigToolchain(path) + override fun deserialize(data: Map): AbstractZigToolchain? { + val location = data["location"]?.toNioPathOrNull() ?: return null + val std = data["std"]?.toNioPathOrNull() + val name = data["name"] + return LocalZigToolchain(location, std, name) } - override fun canSerialize(toolchain: AbstractZigToolchain): Boolean { + override fun isCompatible(toolchain: AbstractZigToolchain): Boolean { return toolchain is LocalZigToolchain } - override fun serialize(toolchain: LocalZigToolchain): JsonElement { - return buildJsonObject { - put("location", toolchain.location.pathString) - } + override fun serialize(toolchain: AbstractZigToolchain): Map { + toolchain as LocalZigToolchain + val map = HashMap() + toolchain.location.pathString.let { map["location"] = it } + toolchain.std?.pathString?.let { map["std"] = it } + toolchain.name?.let { map["name"] = it } + return map + } + + override fun matchesSuggestion( + toolchain: AbstractZigToolchain, + suggestion: AbstractZigToolchain + ): Boolean { + toolchain as LocalZigToolchain + suggestion as LocalZigToolchain + return toolchain.location == suggestion.location + } + + override fun createConfigurable( + uuid: UUID, + toolchain: AbstractZigToolchain, + project: Project + ): NamedConfigurable { + toolchain as LocalZigToolchain + return LocalZigToolchainConfigurable(uuid, toolchain, project) + } + + override fun suggestToolchains(): List { + val res = HashSet() + EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } + return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigSDKType.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigSDKType.kt deleted file mode 100644 index 31f25638..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigSDKType.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain - -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.AdditionalDataConfigurable -import com.intellij.openapi.projectRoots.SdkAdditionalData -import com.intellij.openapi.projectRoots.SdkModel -import com.intellij.openapi.projectRoots.SdkModificator -import com.intellij.openapi.projectRoots.SdkType -import com.intellij.openapi.util.UserDataHolderBase -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.util.EnvironmentUtil -import com.intellij.util.asSafely -import com.intellij.util.system.OS -import kotlinx.coroutines.runBlocking -import org.jdom.Element -import org.jetbrains.annotations.Nls -import java.io.File -import kotlin.io.path.pathString - -class ZigSDKType: SdkType("Zig") { - override fun suggestHomePath(): String? { - return null - } - - private fun getPathEnv(path: String): ZigToolchainEnvironmentSerializable? { - return LocalZigToolchain.tryFromPathString(path)?.zig?.let { runBlocking { it.getEnv(null) } }?.getOrNull() - } - - override fun isValidSdkHome(path: String): Boolean { - return LocalZigToolchain.tryFromPathString(path) != null - } - - override fun suggestSdkName(currentSdkName: String?, sdkHome: String): String { - return getVersionString(sdkHome)?.let { "Zig $it" } ?: currentSdkName ?: "Zig" - } - - override fun getVersionString(sdkHome: String): String? { - return getPathEnv(sdkHome)?.version - } - - override fun suggestHomePaths(): Collection { - val res = HashSet() - EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } - if (OS.CURRENT != OS.Windows) { - EnvironmentUtil.getValue("HOME")?.let { res.add("$it/.local/share/zigup") } - EnvironmentUtil.getValue("XDG_DATA_HOME")?.let { res.add("$it/zigup") } - } - return res - } - - override fun createAdditionalDataConfigurable( - sdkModel: SdkModel, - sdkModificator: SdkModificator - ): AdditionalDataConfigurable? { - return null - } - - override fun getPresentableName(): @Nls(capitalization = Nls.Capitalization.Title) String { - return "Zig" - } - - override fun saveAdditionalData(additionalData: SdkAdditionalData, additional: Element) { - - } - - override fun isRelevantForFile(project: Project, file: VirtualFile): Boolean { - return file.extension == "zig" || file.extension == "zon" - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainList.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainList.kt deleted file mode 100644 index 62fac96a..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainList.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain - -import com.intellij.util.xmlb.annotations.OptionTag - -data class ZigToolchainList( - @get:OptionTag(converter = ZigToolchainListConverter::class) - var toolchains: List = emptyList(), -) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt index 6ff47dcd..778b1b64 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt @@ -23,17 +23,51 @@ package com.falsepattern.zigbrains.project.toolchain import com.falsepattern.zigbrains.ZigBrainsBundle +import com.intellij.ide.projectView.TreeStructureProvider +import com.intellij.ide.util.treeView.AbstractTreeStructure +import com.intellij.ide.util.treeView.AbstractTreeStructureBase import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.roots.ui.SdkAppearanceService +import com.intellij.openapi.roots.ui.configuration.SdkListPresenter import com.intellij.openapi.roots.ui.configuration.SdkPopupFactory import com.intellij.openapi.ui.MasterDetailsComponent -import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.util.BaseTreePopupStep +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.CollectionListModel +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBPanel +import com.intellij.ui.components.panels.HorizontalLayout +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.gridLayout.UnscaledGaps +import com.intellij.ui.popup.list.ComboBoxPopup +import com.intellij.ui.treeStructure.SimpleNode +import com.intellij.ui.treeStructure.SimpleTreeStructure import com.intellij.util.IconUtil +import com.intellij.util.ui.EmptyIcon +import com.intellij.util.ui.UIUtil.FontColor +import java.awt.Component +import java.awt.LayoutManager +import java.util.* +import java.util.function.Consumer +import javax.swing.AbstractListModel +import javax.swing.BoxLayout +import javax.swing.DefaultListModel import javax.swing.JComponent +import javax.swing.JList +import javax.swing.JPanel +import javax.swing.ListCellRenderer +import javax.swing.ListModel +import javax.swing.SwingConstants import javax.swing.tree.DefaultTreeModel +import kotlin.io.path.pathString class ZigToolchainListEditor(): MasterDetailsComponent() { private var isTreeInitialized = false @@ -46,29 +80,62 @@ class ZigToolchainListEditor(): MasterDetailsComponent() { return super.createComponent() } + class ToolchainContext(private val project: Project?, private val model: ListModel): ComboBoxPopup.Context { + override fun getProject(): Project? { + return project + } + + override fun getModel(): ListModel { + return model + } + + override fun getRenderer(): ListCellRenderer { + return object: ColoredListCellRenderer() { + override fun customizeCellRenderer( + list: JList, + value: Any?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + icon = EMPTY_ICON + if (value is LocalZigToolchain) { + icon = IconUtil.addIcon + append(SdkListPresenter.presentDetectedSdkPath(value.location.pathString)) + if (value.name != null) { + append(" ") + append(value.name, SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + } + } + } + } + + } + + class ToolchainPopup(context: ToolchainContext, + selected: Any?, + onItemSelected: Consumer + ): ComboBoxPopup(context, selected, onItemSelected) { + + } + override fun createActions(fromPopup: Boolean): List? { val add = object : DumbAwareAction({"lmaoo"}, Presentation.NULL_STRING, IconUtil.addIcon) { override fun actionPerformed(e: AnActionEvent) { - SdkPopupFactory - .newBuilder() - .withSdkTypeFilter { it is ZigSDKType } - .onSdkSelected { - val path = it.homePath?.toNioPathOrNull() ?: return@onSdkSelected - val toolchain = LocalZigToolchain(path) - zigToolchainList.state = ZigToolchainList(zigToolchainList.state.toolchains + listOf(toolchain)) - addLocalToolchain(toolchain) - (myTree.model as DefaultTreeModel).reload() - } - .buildPopup() - .showPopup(e) + val toolchains = suggestZigToolchains(zigToolchainList.toolchains.map { it.second }.toList()) + val final = ArrayList() + final.addAll(toolchains) + val popup = ToolchainPopup(ToolchainContext(null, CollectionListModel(final)), null, {}) + popup.showInBestPositionFor(e.dataContext) } } return listOf(add, MyDeleteAction()) } override fun onItemDeleted(item: Any?) { - if (item is AbstractZigToolchain) { - zigToolchainList.state = ZigToolchainList(zigToolchainList.state.toolchains.filter { it != item }) + if (item is UUID) { + zigToolchainList.removeToolchain(item) } super.onItemDeleted(item) } @@ -82,18 +149,20 @@ class ZigToolchainListEditor(): MasterDetailsComponent() { override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") - private fun addLocalToolchain(toolchain: LocalZigToolchain) { - val node = MyNode(LocalZigToolchainConfigurable(toolchain, ProjectManager.getInstance().defaultProject)) + private fun addLocalToolchain(uuid: UUID, toolchain: LocalZigToolchain) { + val node = MyNode(LocalZigToolchainConfigurable(uuid, toolchain, ProjectManager.getInstance().defaultProject)) addNode(node, myRoot) } private fun reloadTree() { myRoot.removeAllChildren() - zigToolchainList.state.toolchains.forEach { toolchain -> + zigToolchainList.toolchains.forEach { (uuid, toolchain) -> if (toolchain is LocalZigToolchain) { - addLocalToolchain(toolchain) + addLocalToolchain(uuid, toolchain) } } (myTree.model as DefaultTreeModel).reload() } -} \ No newline at end of file +} + +private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index 5c301b90..b999d1b5 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -22,37 +22,50 @@ package com.falsepattern.zigbrains.project.toolchain -import com.falsepattern.zigbrains.project.settings.ZigProjectSettings -import com.falsepattern.zigbrains.project.toolchain.stdlib.ZigSyntheticLibrary -import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.components.* -import com.intellij.openapi.project.Project -import kotlinx.coroutines.launch +import com.intellij.util.xmlb.annotations.MapAnnotation +import java.util.UUID @Service(Service.Level.APP) @State( - name = "ZigProjectSettings", + name = "ZigToolchainList", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainListService(): PersistentStateComponent { - @Volatile - private var state = ZigToolchainList() - - override fun getState(): ZigToolchainList { - return state.copy() +class ZigToolchainListService: SerializablePersistentStateComponent(State()) { + fun setToolchain(uuid: UUID, toolchain: AbstractZigToolchain) { + updateState { + val newMap = HashMap() + newMap.putAll(it.toolchains) + newMap.put(uuid.toString(), toolchain.toRef()) + it.copy(toolchains = newMap) + } } - fun setState(value: ZigToolchainList) { - this.state = value + fun getToolchain(uuid: UUID): AbstractZigToolchain? { + return state.toolchains[uuid.toString()]?.resolve() } - override fun loadState(state: ZigToolchainList) { - setState(state) + fun removeToolchain(uuid: UUID) { + val str = uuid.toString() + updateState { + it.copy(toolchains = it.toolchains.filter { it.key != str }) + } } - fun isModified(otherData: ZigToolchainList): Boolean { - return state != otherData - } + val toolchains: Sequence> + get() = state.toolchains + .asSequence() + .mapNotNull { + val uuid = UUID.fromString(it.key) ?: return@mapNotNull null + val tc = it.value.resolve() ?: return@mapNotNull null + uuid to tc + } + + data class State( + @JvmField + @MapAnnotation(surroundKeyWithTag = false, surroundValueWithTag = false) + val toolchains: Map = emptyMap(), + ) } val zigToolchainList get() = service() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt index 1ad68e8d..f6c7eccf 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt @@ -22,71 +22,57 @@ package com.falsepattern.zigbrains.project.toolchain -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider.Companion.EXTENSION_POINT_NAME import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.UserDataHolder -import com.intellij.util.asSafely -import com.intellij.util.xmlb.Converter -import kotlinx.serialization.json.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.flattenConcat +import kotlinx.coroutines.flow.map +import java.util.UUID -sealed interface ZigToolchainProvider { +private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") + +sealed interface ZigToolchainProvider { suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain? val serialMarker: String - fun deserialize(data: JsonElement): AbstractZigToolchain? - fun canSerialize(toolchain: AbstractZigToolchain): Boolean - fun serialize(toolchain: T): JsonElement - - companion object { - val EXTENSION_POINT_NAME = ExtensionPointName.create>("com.falsepattern.zigbrains.toolchainProvider") - - suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain? { - return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(project, extraData) } - } - - fun fromJson(json: JsonObject): AbstractZigToolchain? { - val marker = (json["marker"] as? JsonPrimitive)?.contentOrNull ?: return null - val data = json["data"] ?: return null - val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null - return provider.deserialize(data) - } - - fun toJson(tc: AbstractZigToolchain): JsonObject? { - val provider = EXTENSION_POINT_NAME.extensionList.find { it.canSerialize(tc) } ?: return null - return buildJsonObject { - put("marker", provider.serialMarker) - put("data", provider.serialize(tc)) - } - } - } + fun isCompatible(toolchain: AbstractZigToolchain): Boolean + fun deserialize(data: Map): AbstractZigToolchain? + fun serialize(toolchain: AbstractZigToolchain): Map + fun matchesSuggestion(toolchain: AbstractZigToolchain, suggestion: AbstractZigToolchain): Boolean + fun createConfigurable(uuid: UUID, toolchain: AbstractZigToolchain, project: Project): NamedConfigurable + fun suggestToolchains(): List } -@Suppress("UNCHECKED_CAST") -private fun ZigToolchainProvider.serialize(toolchain: AbstractZigToolchain) = serialize(toolchain as T) - -class ZigToolchainConverter: Converter() { - override fun fromString(value: String): AbstractZigToolchain? { - val json = Json.parseToJsonElement(value) as? JsonObject ?: return null - return ZigToolchainProvider.fromJson(json) - } - - override fun toString(value: AbstractZigToolchain): String? { - return ZigToolchainProvider.toJson(value)?.toString() - } +fun AbstractZigToolchain.Ref.resolve(): AbstractZigToolchain? { + val marker = this.marker ?: return null + val data = this.data ?: return null + val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null + return provider.deserialize(data) } -class ZigToolchainListConverter: Converter>() { - override fun fromString(value: String): List { - val json = Json.parseToJsonElement(value) as? JsonArray ?: return emptyList() - return json.mapNotNull { it.asSafely()?.let { ZigToolchainProvider.fromJson(it) } } - } +fun AbstractZigToolchain.toRef(): AbstractZigToolchain.Ref { + val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() + return AbstractZigToolchain.Ref(provider.serialMarker, provider.serialize(this)) +} - override fun toString(value: List): String { - return buildJsonArray { - value.mapNotNull { ZigToolchainProvider.toJson(it) }.forEach { - add(it) - } - }.toString() +suspend fun Project?.suggestZigToolchain(extraData: UserDataHolder): AbstractZigToolchain? { + return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(this, extraData) } +} + +fun AbstractZigToolchain.createNamedConfigurable(uuid: UUID, project: Project): NamedConfigurable { + val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() + return provider.createConfigurable(uuid, this, project) +} + +fun suggestZigToolchains(existing: List): List { + return EXTENSION_POINT_NAME.extensionList.flatMap { ext -> + val compatibleExisting = existing.filter { ext.isCompatible(it) } + val suggestions = ext.suggestToolchains() + suggestions.filter { suggestion -> compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } } } } \ No newline at end of file diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index ec6a4f81..b5ec5fe5 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -150,7 +150,6 @@ id="ZigToolchainConfigurable" displayName="Toolchain" /> - throwable.printStackTrace() From 2c500d40a5515bc5c492580a790aae3f7b4c8992 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Sun, 6 Apr 2025 02:25:22 +0200 Subject: [PATCH 03/22] work work work --- .../project/toolchain/LocalZigToolchain.kt | 9 +- .../toolchain/LocalZigToolchainProvider.kt | 25 ++ .../toolchain/ZigToolchainListEditor.kt | 336 ++++++++++++++---- .../toolchain/ZigToolchainListService.kt | 1 - .../project/toolchain/ZigToolchainProvider.kt | 7 + .../project/toolchain/ZigVersionInfo.kt | 106 ++++++ .../toolchain/tools/ZigCompilerTool.kt | 3 + .../shared/coroutine/CoroutinesUtil.kt | 6 + 8 files changed, 412 insertions(+), 81 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt index 2eeca5fe..397bd5f3 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchain.kt @@ -32,6 +32,7 @@ import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull +import kotlinx.coroutines.runBlocking import java.nio.file.Path import java.util.UUID import kotlin.io.path.pathString @@ -71,10 +72,16 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, val name } fun tryFromPath(path: Path): LocalZigToolchain? { - val tc = LocalZigToolchain(path) + var tc = LocalZigToolchain(path) if (!tc.zig.fileValid()) { return null } + tc.zig + .getEnvBlocking(null) + .getOrNull() + ?.version + ?.let { "Zig $it" } + ?.let { tc = tc.copy(name = it) } return tc } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt index d49f652b..d83e8419 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt @@ -30,8 +30,13 @@ import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.ui.configuration.SdkPopupBuilder import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.openapi.util.text.StringUtil +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.SimpleTextAttributes import com.intellij.util.EnvironmentUtil +import com.intellij.util.IconUtil import com.intellij.util.system.OS import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow @@ -98,4 +103,24 @@ class LocalZigToolchainProvider: ZigToolchainProvider { EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) } } + + override fun render(toolchain: AbstractZigToolchain, component: SimpleColoredComponent) { + toolchain as LocalZigToolchain + component.append(presentDetectedPath(toolchain.location.pathString)) + if (toolchain.name != null) { + component.append(" ") + component.append(toolchain.name, SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + } +} + + +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/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt index 778b1b64..a006aed4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt @@ -22,111 +22,94 @@ package com.falsepattern.zigbrains.project.toolchain +import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.ZigBrainsBundle -import com.intellij.ide.projectView.TreeStructureProvider -import com.intellij.ide.util.treeView.AbstractTreeStructure -import com.intellij.ide.util.treeView.AbstractTreeStructureBase +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.icons.AllIcons 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.application.PathManager +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.progress.coroutineToIndicator import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.roots.ui.SdkAppearanceService -import com.intellij.openapi.roots.ui.configuration.SdkListPresenter -import com.intellij.openapi.roots.ui.configuration.SdkPopupFactory +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.MasterDetailsComponent -import com.intellij.openapi.ui.popup.JBPopupFactory -import com.intellij.openapi.ui.popup.util.BaseTreePopupStep -import com.intellij.openapi.util.NlsContexts -import com.intellij.ui.CollectionListModel -import com.intellij.ui.ColoredListCellRenderer -import com.intellij.ui.SimpleTextAttributes -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.JBPanel -import com.intellij.ui.components.panels.HorizontalLayout +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.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.platform.util.progress.withProgressText +import com.intellij.ui.* +import com.intellij.ui.components.JBList +import com.intellij.ui.components.panels.OpaquePanel +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.Align +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.dsl.gridLayout.UnscaledGaps import com.intellij.ui.popup.list.ComboBoxPopup -import com.intellij.ui.treeStructure.SimpleNode -import com.intellij.ui.treeStructure.SimpleTreeStructure +import com.intellij.util.Consumer import com.intellij.util.IconUtil +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.system.CpuArch +import com.intellij.util.text.SemVer import com.intellij.util.ui.EmptyIcon -import com.intellij.util.ui.UIUtil.FontColor +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromStream +import java.awt.BorderLayout import java.awt.Component -import java.awt.LayoutManager +import java.nio.file.Path import java.util.* -import java.util.function.Consumer -import javax.swing.AbstractListModel -import javax.swing.BoxLayout -import javax.swing.DefaultListModel +import javax.accessibility.AccessibleContext +import javax.swing.DefaultComboBoxModel import javax.swing.JComponent import javax.swing.JList import javax.swing.JPanel import javax.swing.ListCellRenderer -import javax.swing.ListModel -import javax.swing.SwingConstants +import javax.swing.border.Border +import javax.swing.event.DocumentEvent import javax.swing.tree.DefaultTreeModel -import kotlin.io.path.pathString -class ZigToolchainListEditor(): MasterDetailsComponent() { +class ZigToolchainListEditor() : MasterDetailsComponent() { private var isTreeInitialized = false + private var myComponent: JComponent? = null override fun createComponent(): JComponent { if (!isTreeInitialized) { initTree() isTreeInitialized = true } - return super.createComponent() + val comp = super.createComponent() + myComponent = comp + return comp } - class ToolchainContext(private val project: Project?, private val model: ListModel): ComboBoxPopup.Context { - override fun getProject(): Project? { - return project - } - - override fun getModel(): ListModel { - return model - } - - override fun getRenderer(): ListCellRenderer { - return object: ColoredListCellRenderer() { - override fun customizeCellRenderer( - list: JList, - value: Any?, - index: Int, - selected: Boolean, - hasFocus: Boolean - ) { - icon = EMPTY_ICON - if (value is LocalZigToolchain) { - icon = IconUtil.addIcon - append(SdkListPresenter.presentDetectedSdkPath(value.location.pathString)) - if (value.name != null) { - append(" ") - append(value.name, SimpleTextAttributes.GRAYED_ATTRIBUTES) - } - } - } - } - } - - } - - class ToolchainPopup(context: ToolchainContext, - selected: Any?, - onItemSelected: Consumer - ): ComboBoxPopup(context, selected, onItemSelected) { - - } - - override fun createActions(fromPopup: Boolean): List? { - val add = object : DumbAwareAction({"lmaoo"}, Presentation.NULL_STRING, IconUtil.addIcon) { + override fun createActions(fromPopup: Boolean): List { + val add = object : DumbAwareAction({ "lmaoo" }, Presentation.NULL_STRING, IconUtil.addIcon) { override fun actionPerformed(e: AnActionEvent) { val toolchains = suggestZigToolchains(zigToolchainList.toolchains.map { it.second }.toList()) - val final = ArrayList() - final.addAll(toolchains) - val popup = ToolchainPopup(ToolchainContext(null, CollectionListModel(final)), null, {}) + val final = ArrayList() + final.add(TCListElem.Download) + final.add(TCListElem.FromDisk) + final.add(Separator("Detected toolchains", true)) + final.addAll(toolchains.map { TCListElem.Toolchain(it) }) + val model = TCModel(final) + val context = TCContext(null, model) + val popup = TCPopup(context, null, ::onItemSelected) popup.showInBestPositionFor(e.dataContext) } } @@ -140,6 +123,61 @@ class ZigToolchainListEditor(): MasterDetailsComponent() { super.onItemDeleted(item) } + private fun onItemSelected(elem: TCListElem) { + when (elem) { + is TCListElem.Toolchain -> { + val uuid = UUID.randomUUID() + zigToolchainList.setToolchain(uuid, elem.toolchain) + addToolchain(uuid, elem.toolchain) + (myTree.model as DefaultTreeModel).reload() + } + + is TCListElem.Download -> { + zigCoroutineScope.async { + withEDTContext(ModalityState.stateForComponent(myComponent!!)) { + val info = withModalProgress(myComponent?.let { ModalTaskOwner.component(it) } ?: ModalTaskOwner.guess(), "Fetching zig version information", TaskCancellation.cancellable()) { + ZigVersionInfo.download() + } + val dialog = DialogBuilder() + val theList = ComboBox(DefaultComboBoxModel(info.map { it.first }.toTypedArray())) + val outputPath = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + ).also { + Disposer.register(dialog, it) + } + var archiveSizeCell: Cell<*>? = null + fun detect(item: String) { + outputPath.text = System.getProperty("user.home") + "/.zig/" + item + val data = info.firstOrNull { it.first == item } ?: return + val size = data.second.dist.size + val sizeMb = size / (1024f * 1024f) + archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) + } + theList.addItemListener { + detect(it.item as String) + } + val center = panel { + row("Version:") { + cell(theList).resizableColumn().align(AlignX.FILL) + } + row("Location:") { + cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } + } + } + detect(info[0].first) + dialog.centerPanel(center) + dialog.setTitle("Version Selector") + dialog.addCancelAction() + dialog.showAndGet() + } + } + } + + is TCListElem.FromDisk -> {} + } + } + override fun reset() { reloadTree() super.reset() @@ -149,20 +187,160 @@ class ZigToolchainListEditor(): MasterDetailsComponent() { override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") - private fun addLocalToolchain(uuid: UUID, toolchain: LocalZigToolchain) { - val node = MyNode(LocalZigToolchainConfigurable(uuid, toolchain, ProjectManager.getInstance().defaultProject)) + private fun addToolchain(uuid: UUID, toolchain: AbstractZigToolchain) { + val node = MyNode(toolchain.createNamedConfigurable(uuid, ProjectManager.getInstance().defaultProject)) addNode(node, myRoot) } private fun reloadTree() { myRoot.removeAllChildren() zigToolchainList.toolchains.forEach { (uuid, toolchain) -> - if (toolchain is LocalZigToolchain) { - addLocalToolchain(uuid, toolchain) - } + addToolchain(uuid, toolchain) } (myTree.model as DefaultTreeModel).reload() } + + override fun disposeUIResources() { + super.disposeUIResources() + myComponent = null + } +} + +private sealed interface TCListElemIn + +private sealed interface TCListElem : TCListElemIn { + @JvmRecord + data class Toolchain(val toolchain: AbstractZigToolchain) : TCListElem + object Download : TCListElem + object FromDisk : TCListElem +} + +@JvmRecord +private data class Separator(val text: String, val separatorBar: Boolean) : TCListElemIn + +private class TCPopup( + context: TCContext, + selected: TCListElem?, + onItemSelected: Consumer, +) : ComboBoxPopup(context, selected, onItemSelected) + +private class TCModel private constructor(elements: List, private val separators: Map) : CollectionListModel(elements) { + companion object { + operator fun invoke(input: List): TCModel { + val separators = IdentityHashMap() + var lastSeparator: Separator? = null + val elements = ArrayList() + input.forEach { + when (it) { + is TCListElem -> { + if (lastSeparator != null) { + separators[it] = lastSeparator + lastSeparator = null + } + elements.add(it) + } + + is Separator -> lastSeparator = it + } + } + val model = TCModel(elements, separators) + return model + } + } + + fun separatorAbove(elem: TCListElem) = separators[elem] +} + +private class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { + override fun getProject(): Project? { + return project + } + + override fun getModel(): TCModel { + return model + } + + override fun getRenderer(): ListCellRenderer { + return TCCellRenderer(::getModel) + } +} + +private class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer() { + + override fun getListCellRendererComponent( + list: JList?, + value: TCListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ): Component? { + val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent + val panel = object : CellRendererPanel(BorderLayout()) { + val myContext = component.accessibleContext + + override fun getAccessibleContext(): AccessibleContext? { + return myContext + } + + override fun setBorder(border: Border?) { + component.border = border + } + } + panel.add(component, BorderLayout.CENTER) + + component.isOpaque = true + list?.let { background = if (selected) it.selectionBackground else it.background } + + val model = getModel() + + val separator = value?.let { model.separatorAbove(it) } + + if (separator != null) { + val separatorText = separator.text + val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 + val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) + separatorComponent.isHideLine = !separator.separatorBar + if (separatorText.isNotBlank()) { + separatorComponent.caption = separatorText + } + + val wrapper = OpaquePanel(BorderLayout()) + wrapper.add(separatorComponent, BorderLayout.CENTER) + list?.let { wrapper.background = it.background } + panel.add(wrapper, BorderLayout.NORTH) + } + + return panel + } + + override fun customizeCellRenderer( + list: JList, + value: TCListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + icon = EMPTY_ICON + when (value) { + is TCListElem.Toolchain -> { + icon = Icons.Zig + val toolchain = value.toolchain + toolchain.render(this) + } + + is TCListElem.Download -> { + icon = AllIcons.Actions.Download + append("Download Zig\u2026") + } + + is TCListElem.FromDisk -> { + icon = AllIcons.General.OpenDisk + append("Add Zig from disk\u2026") + } + + null -> {} + } + } } private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index b999d1b5..b38bca87 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -63,7 +63,6 @@ class ZigToolchainListService: SerializablePersistentStateComponent = emptyMap(), ) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt index f6c7eccf..7c4ded3b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt @@ -26,6 +26,7 @@ import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.UserDataHolder +import com.intellij.ui.SimpleColoredComponent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.filter @@ -46,6 +47,7 @@ sealed interface ZigToolchainProvider { fun matchesSuggestion(toolchain: AbstractZigToolchain, suggestion: AbstractZigToolchain): Boolean fun createConfigurable(uuid: UUID, toolchain: AbstractZigToolchain, project: Project): NamedConfigurable fun suggestToolchains(): List + fun render(toolchain: AbstractZigToolchain, component: SimpleColoredComponent) } fun AbstractZigToolchain.Ref.resolve(): AbstractZigToolchain? { @@ -75,4 +77,9 @@ fun suggestZigToolchains(existing: List): List compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } } } +} + +fun AbstractZigToolchain.render(component: SimpleColoredComponent) { + val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() + return provider.render(this, component) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt new file mode 100644 index 00000000..dcfc7758 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt @@ -0,0 +1,106 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.progress.coroutineToIndicator +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.platform.util.progress.withProgressText +import com.intellij.util.asSafely +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.system.CpuArch +import com.intellij.util.system.OS +import com.intellij.util.text.SemVer +import com.jetbrains.rd.util.firstOrNull +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.decodeFromStream +import java.nio.file.Path + +@JvmRecord +data class ZigVersionInfo(val date: String, val docs: String, val notes: String, val src: Tarball?, val dist: Tarball) { + companion object { + @OptIn(ExperimentalSerializationApi::class) + suspend fun download(): List> { + return withProgressText("Fetching zig version information") { + withContext(Dispatchers.IO) { + val service = DownloadableFileService.getInstance() + val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json") + val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") + val downloadDirectory = tempPluginDir.toFile() + val downloadResults = coroutineToIndicator { + downloader.download(downloadDirectory) + } + var info: JsonObject? = null + for (result in downloadResults) { + if (result.second.defaultFileName == "index.json") { + info = result.first.inputStream().use { Json.decodeFromStream(it) } + } + } + info?.mapNotNull getVersions@{ (version, data) -> + data as? JsonObject ?: return@getVersions null + val date = data["date"]?.asSafely()?.content ?: "" + val docs = data["docs"]?.asSafely()?.content ?: "" + val notes = data["notes"]?.asSafely()?.content ?: "" + val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } + val dist = data.firstNotNullOfOrNull findCompatible@{ (dist, tb) -> + if (!dist.contains('-')) + return@findCompatible null + val (arch, os) = dist.split('-', limit = 2) + val theArch = when(arch) { + "x86_64" -> CpuArch.X86_64 + "i386" -> CpuArch.X86 + "armv7a" -> CpuArch.ARM32 + "aarch64" -> CpuArch.ARM64 + else -> return@findCompatible null + } + val theOS = when(os) { + "linux" -> OS.Linux + "windows" -> OS.Windows + "macos" -> OS.macOS + "freebsd" -> OS.FreeBSD + else -> return@findCompatible null + } + if (theArch == CpuArch.CURRENT && theOS == OS.CURRENT) { + Json.decodeFromJsonElement(tb) + } else null + } ?: return@getVersions null + Pair(version, ZigVersionInfo(date, docs, notes, src, dist)) + } ?.toList() ?: emptyList() + } + } + } + } +} + +@JvmRecord +@Serializable +data class Tarball(val tarball: String, val shasum: String, val size: Int) + +private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains") \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt index 85e2af65..1f67b4a7 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt @@ -25,6 +25,7 @@ package com.falsepattern.zigbrains.project.toolchain.tools import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSerializable import com.intellij.openapi.project.Project +import kotlinx.coroutines.runBlocking import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import java.nio.file.Path @@ -45,6 +46,8 @@ class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) { Result.failure(IllegalStateException("could not deserialize zig env", e)) } } + + fun getEnvBlocking(project: Project?) = runBlocking { getEnv(project) } } private val envJson = Json { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt index cc69c88a..7a8e4c24 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt @@ -49,6 +49,12 @@ suspend inline fun withCurrentEDTModalityContext(noinline block: suspend Cor } } +fun T.letBlocking(targetAction: suspend CoroutineScope.(T) -> R): R { + return runBlocking { + targetAction(this@letBlocking) + } +} + suspend inline fun runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T { return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction) } From 9541bb9752e3105d986853a46fea42d569214438 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Sun, 6 Apr 2025 12:10:31 +0200 Subject: [PATCH 04/22] refactor toolchain directory structure --- .../execution/binary/ZigProfileStateBinary.kt | 4 +- .../base/ZigDebugEmitBinaryInstaller.kt | 4 +- .../runner/base/ZigDebugParametersBase.kt | 4 +- .../base/ZigDebugParametersEmitBinaryBase.kt | 4 +- .../runner/base/ZigDebugRunnerBase.kt | 8 ++-- .../runner/binary/ZigDebugParametersBinary.kt | 4 +- .../runner/binary/ZigDebugRunnerBinary.kt | 6 +-- .../runner/build/ZigDebugParametersBuild.kt | 4 +- .../runner/build/ZigDebugRunnerBuild.kt | 6 +-- .../runner/run/ZigDebugParametersRun.kt | 4 +- .../debugger/runner/run/ZigDebugRunnerRun.kt | 6 +-- .../runner/test/ZigDebugParametersTest.kt | 4 +- .../runner/test/ZigDebugRunnerTest.kt | 6 +-- .../com/falsepattern/zigbrains/ZBStartup.kt | 5 +-- .../project/execution/base/ZigProfileState.kt | 15 +------ .../zigbrains/project/run/ZigProgramRunner.kt | 4 +- .../zigbrains/project/run/ZigRegularRunner.kt | 4 +- .../ZigProjectConfigurationProvider.kt | 4 +- .../project/settings/ZigProjectSettings.kt | 2 +- .../settings/ZigProjectSettingsPanel.kt | 5 +-- .../settings/ZigProjectSettingsService.kt | 2 +- .../stdlib/ZigLibraryRootProvider.kt | 2 +- .../stdlib/ZigSyntheticLibrary.kt | 2 +- .../toolchain/ZigToolchainListEditor.kt | 26 +++--------- .../toolchain/ZigToolchainListService.kt | 14 ++++--- .../ZigToolchain.kt} | 8 +--- .../{ => base}/ZigToolchainProvider.kt | 40 ++++++++----------- .../{ => local}/LocalZigToolchain.kt | 10 ++--- .../LocalZigToolchainConfigurable.kt | 3 +- .../{ => local}/LocalZigToolchainPanel.kt | 2 +- .../{ => local}/LocalZigToolchainProvider.kt | 28 +++++-------- .../toolchain/tools/ZigCompilerTool.kt | 5 +-- .../project/toolchain/tools/ZigTool.kt | 4 +- .../ZigToolchainEnvironmentSerializable.kt | 7 ++-- .../resources/META-INF/zigbrains-core.xml | 4 +- .../ToolchainZLSConfigProvider.kt | 4 +- .../main/resources/META-INF/zigbrains-lsp.xml | 2 +- src/main/resources/META-INF/plugin.xml | 2 +- 38 files changed, 113 insertions(+), 155 deletions(-) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/{toolchain => }/stdlib/ZigLibraryRootProvider.kt (95%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/{toolchain => }/stdlib/ZigSyntheticLibrary.kt (99%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{AbstractZigToolchain.kt => base/ZigToolchain.kt} (89%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ => base}/ZigToolchainProvider.kt (64%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ => local}/LocalZigToolchain.kt (91%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ => local}/LocalZigToolchainConfigurable.kt (95%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ => local}/LocalZigToolchainPanel.kt (99%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ => local}/LocalZigToolchainProvider.kt (81%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ => tools}/ZigToolchainEnvironmentSerializable.kt (93%) rename lsp/src/main/kotlin/com/falsepattern/zigbrains/{project/toolchain => lsp}/ToolchainZLSConfigProvider.kt (96%) diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/execution/binary/ZigProfileStateBinary.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/execution/binary/ZigProfileStateBinary.kt index 4f73207f..3da81d9c 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/execution/binary/ZigProfileStateBinary.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/execution/binary/ZigProfileStateBinary.kt @@ -24,14 +24,14 @@ package com.falsepattern.zigbrains.debugger.execution.binary import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.runners.ExecutionEnvironment import kotlin.io.path.pathString class ZigProfileStateBinary(environment: ExecutionEnvironment, configuration: ZigExecConfigBinary) : ZigProfileState(environment, configuration) { - override suspend fun getCommandLine(toolchain: AbstractZigToolchain, debug: Boolean): GeneralCommandLine { + override suspend fun getCommandLine(toolchain: ZigToolchain, debug: Boolean): GeneralCommandLine { val cli = GeneralCommandLine() val cfg = configuration cfg.workingDirectory.path?.let { cli.withWorkingDirectory(it) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugEmitBinaryInstaller.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugEmitBinaryInstaller.kt index 1f34ca3c..c73c0243 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugEmitBinaryInstaller.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugEmitBinaryInstaller.kt @@ -23,7 +23,7 @@ package com.falsepattern.zigbrains.debugger.runner.base import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.PtyCommandLine @@ -34,7 +34,7 @@ import java.io.File class ZigDebugEmitBinaryInstaller>( private val profileState: ProfileState, - private val toolchain: AbstractZigToolchain, + private val toolchain: ZigToolchain, private val executableFile: File, private val exeArgs: List ): Installer { diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersBase.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersBase.kt index 038c564a..567a2700 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersBase.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersBase.kt @@ -23,7 +23,7 @@ package com.falsepattern.zigbrains.debugger.runner.base import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.util.system.CpuArch import com.jetbrains.cidr.ArchitectureType import com.jetbrains.cidr.execution.RunParameters @@ -31,7 +31,7 @@ import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration abstract class ZigDebugParametersBase>( private val driverConfiguration: DebuggerDriverConfiguration, - protected val toolchain: AbstractZigToolchain, + protected val toolchain: ZigToolchain, protected val profileState: ProfileState ): RunParameters() { override fun getDebuggerDriverConfiguration(): DebuggerDriverConfiguration { diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersEmitBinaryBase.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersEmitBinaryBase.kt index 55bca424..b1e945a8 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersEmitBinaryBase.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugParametersEmitBinaryBase.kt @@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.debugger.runner.base import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.openapi.util.io.FileUtil import com.intellij.platform.util.progress.withProgressText @@ -39,7 +39,7 @@ import kotlin.io.path.isExecutable abstract class ZigDebugParametersEmitBinaryBase>( driverConfiguration: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, profileState: ProfileState, ) : ZigDebugParametersBase(driverConfiguration, toolchain, profileState), PreLaunchAware { @Volatile diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugRunnerBase.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugRunnerBase.kt index 7f176753..9e9cd863 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugRunnerBase.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/base/ZigDebugRunnerBase.kt @@ -26,7 +26,7 @@ import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProv import com.falsepattern.zigbrains.debugger.ZigLocalDebugProcess import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.run.ZigProgramRunner -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.intellij.execution.DefaultExecutionResult @@ -52,7 +52,7 @@ abstract class ZigDebugRunnerBase> : ZigProgra @Throws(ExecutionException::class) override suspend fun execute( state: ProfileState, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, environment: ExecutionEnvironment ): RunContentDescriptor? { val project = environment.project @@ -67,7 +67,7 @@ abstract class ZigDebugRunnerBase> : ZigProgra @Throws(ExecutionException::class) private suspend fun executeWithDriver( state: ProfileState, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, environment: ExecutionEnvironment, debuggerDriver: DebuggerDriverConfiguration ): RunContentDescriptor? { @@ -113,7 +113,7 @@ abstract class ZigDebugRunnerBase> : ZigProgra protected abstract fun getDebugParameters( state: ProfileState, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase private class SharedConsoleBuilder(private val console: ConsoleView) : TextConsoleBuilder() { diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugParametersBinary.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugParametersBinary.kt index 95620485..887b8cf6 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugParametersBinary.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugParametersBinary.kt @@ -26,13 +26,13 @@ import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinary import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration -class ZigDebugParametersBinary @Throws(ExecutionException::class) constructor(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateBinary) : +class ZigDebugParametersBinary @Throws(ExecutionException::class) constructor(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateBinary) : ZigDebugParametersBase(driverConfiguration, toolchain, profileState) { private val executableFile = profileState.configuration.exePath.path?.toFile() ?: throw ExecutionException(ZigDebugBundle.message("exception.missing-exe-path")) override fun getInstaller(): Installer { diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugRunnerBinary.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugRunnerBinary.kt index 6da574e4..34f7115a 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugRunnerBinary.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/binary/ZigDebugRunnerBinary.kt @@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinar import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.execution.configurations.RunProfile import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration @@ -36,7 +36,7 @@ class ZigDebugRunnerBinary: ZigDebugRunnerBase() { override fun getDebugParameters( state: ZigProfileStateBinary, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase { return ZigDebugParametersBinary(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugParametersBuild.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugParametersBuild.kt index aa9e2e38..6dcda7be 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugParametersBuild.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugParametersBuild.kt @@ -28,7 +28,7 @@ import com.falsepattern.zigbrains.debugger.runner.base.PreLaunchProcessListener import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.openapi.util.SystemInfo import com.intellij.platform.util.progress.withProgressText @@ -46,7 +46,7 @@ import kotlin.io.path.isRegularFile class ZigDebugParametersBuild( driverConfiguration: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, profileState: ZigProfileStateBuild ) : ZigDebugParametersBase(driverConfiguration, toolchain, profileState), PreLaunchAware { @Volatile diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugRunnerBuild.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugRunnerBuild.kt index 575ab657..97f99cff 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugRunnerBuild.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/build/ZigDebugRunnerBuild.kt @@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.build.ZigExecConfigBuild import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.execution.configurations.RunProfile import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration @@ -36,7 +36,7 @@ class ZigDebugRunnerBuild: ZigDebugRunnerBase() { override fun getDebugParameters( state: ZigProfileStateBuild, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase { return ZigDebugParametersBuild(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugParametersRun.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugParametersRun.kt index 32558668..01f9809e 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugParametersRun.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugParametersRun.kt @@ -25,11 +25,11 @@ package com.falsepattern.zigbrains.debugger.runner.run import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersEmitBinaryBase import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration -class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateRun) : +class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateRun) : ZigDebugParametersEmitBinaryBase(driverConfiguration, toolchain, profileState) { override fun getInstaller(): Installer { return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit()) diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugRunnerRun.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugRunnerRun.kt index 9cc440d8..492492e0 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugRunnerRun.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/run/ZigDebugRunnerRun.kt @@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.run.ZigExecConfigRun import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.execution.configurations.RunProfile import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration @@ -36,7 +36,7 @@ class ZigDebugRunnerRun: ZigDebugRunnerBase() { override fun getDebugParameters( state: ZigProfileStateRun, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase { return ZigDebugParametersRun(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) } diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugParametersTest.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugParametersTest.kt index 7f45423c..9044196e 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugParametersTest.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugParametersTest.kt @@ -25,11 +25,11 @@ package com.falsepattern.zigbrains.debugger.runner.test import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersEmitBinaryBase import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration -class ZigDebugParametersTest(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateTest) : +class ZigDebugParametersTest(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateTest) : ZigDebugParametersEmitBinaryBase(driverConfiguration, toolchain, profileState) { override fun getInstaller(): Installer { return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, listOf()) diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugRunnerTest.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugRunnerTest.kt index f4673d6d..f01a98c7 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugRunnerTest.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/runner/test/ZigDebugRunnerTest.kt @@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.test.ZigExecConfigTest import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.execution.configurations.RunProfile import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration @@ -36,7 +36,7 @@ class ZigDebugRunnerTest: ZigDebugRunnerBase() { override fun getDebugParameters( state: ZigProfileStateTest, debuggerDriver: DebuggerDriverConfiguration, - toolchain: AbstractZigToolchain + toolchain: ZigToolchain ): ZigDebugParametersBase { return ZigDebugParametersTest(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt index fa4d6f56..5df8c86c 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt @@ -24,9 +24,8 @@ package com.falsepattern.zigbrains import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider -import com.falsepattern.zigbrains.project.toolchain.suggestZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain import com.intellij.ide.BrowserUtil import com.intellij.ide.plugins.PluginManager import com.intellij.notification.Notification diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt index 251ca441..c6779014 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt @@ -24,28 +24,17 @@ package com.falsepattern.zigbrains.project.execution.base import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.execution.ZigConsoleBuilder -import com.falsepattern.zigbrains.project.run.ZigProcessHandler import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking -import com.falsepattern.zigbrains.shared.ipc.IPCUtil -import com.falsepattern.zigbrains.shared.ipc.ipc -import com.intellij.build.BuildTextConsoleView -import com.intellij.execution.DefaultExecutionResult import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.CommandLineState import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.PtyCommandLine -import com.intellij.execution.filters.TextConsoleBuilder import com.intellij.execution.process.ProcessHandler -import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.execution.runners.ExecutionEnvironment -import com.intellij.openapi.project.Project import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.terminal.TerminalExecutionConsole -import com.intellij.util.system.OS -import kotlin.collections.contains import kotlin.io.path.pathString abstract class ZigProfileState> ( @@ -71,7 +60,7 @@ abstract class ZigProfileState> ( } @Throws(ExecutionException::class) - open suspend fun getCommandLine(toolchain: AbstractZigToolchain, debug: Boolean): GeneralCommandLine { + open suspend fun getCommandLine(toolchain: ZigToolchain, debug: Boolean): GeneralCommandLine { val workingDir = configuration.workingDirectory val zigExePath = toolchain.zig.path() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigProgramRunner.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigProgramRunner.kt index e897c893..c02fa66d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigProgramRunner.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigProgramRunner.kt @@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.project.run import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.RunProfileState @@ -81,5 +81,5 @@ abstract class ZigProgramRunner>(protected val protected abstract fun castProfileState(state: ZigProfileState<*>): ProfileState? @Throws(ExecutionException::class) - abstract suspend fun execute(state: ProfileState, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? + abstract suspend fun execute(state: ProfileState, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt index 771e94d2..1860ab1f 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt @@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.project.run import com.falsepattern.zigbrains.project.execution.base.ZigExecConfig import com.falsepattern.zigbrains.project.execution.base.ZigProfileState -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.intellij.execution.configurations.RunProfile import com.intellij.execution.executors.DefaultRunExecutor @@ -34,7 +34,7 @@ import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.application.ModalityState class ZigRegularRunner: ZigProgramRunner>(DefaultRunExecutor.EXECUTOR_ID) { - override suspend fun execute(state: ZigProfileState<*>, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? { + override suspend fun execute(state: ZigProfileState<*>, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? { val exec = state.execute(environment.executor, this) return withEDTContext(ModalityState.any()) { val runContentBuilder = RunContentBuilder(exec, environment) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt index a609a3ee..af4c481d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.project.settings -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.SubConfigurable import com.intellij.openapi.Disposable import com.intellij.openapi.extensions.ExtensionPointName @@ -58,6 +58,6 @@ interface ZigProjectConfigurationProvider { fun apply(project: Project) } interface ToolchainProvider { - val toolchain: AbstractZigToolchain? + val toolchain: ZigToolchain? } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt index facf092c..d97af0cd 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.project.settings -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.util.xmlb.annotations.Transient diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt index b9a388a1..1f3271c4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt @@ -24,9 +24,8 @@ package com.falsepattern.zigbrains.project.settings import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider -import com.falsepattern.zigbrains.project.toolchain.suggestZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt index 6820c488..71b337d0 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.project.settings -import com.falsepattern.zigbrains.project.toolchain.stdlib.ZigSyntheticLibrary +import com.falsepattern.zigbrains.project.stdlib.ZigSyntheticLibrary import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.components.* import com.intellij.openapi.project.Project diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigLibraryRootProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigLibraryRootProvider.kt similarity index 95% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigLibraryRootProvider.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigLibraryRootProvider.kt index e8ce9239..4cceeae4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigLibraryRootProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigLibraryRootProvider.kt @@ -20,7 +20,7 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.stdlib +package com.falsepattern.zigbrains.project.stdlib import com.intellij.openapi.project.Project import com.intellij.openapi.roots.AdditionalLibraryRootsProvider diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigSyntheticLibrary.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt similarity index 99% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigSyntheticLibrary.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt index 5b7fac8d..dd73c2cc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/stdlib/ZigSyntheticLibrary.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt @@ -20,7 +20,7 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.stdlib +package com.falsepattern.zigbrains.project.stdlib import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.project.settings.ZigProjectSettings diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt index a006aed4..fb917897 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt @@ -24,6 +24,10 @@ package com.falsepattern.zigbrains.project.toolchain 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.createNamedConfigurable +import com.falsepattern.zigbrains.project.toolchain.base.render +import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.icons.AllIcons @@ -31,9 +35,7 @@ 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.application.PathManager import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.progress.coroutineToIndicator import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager @@ -41,47 +43,31 @@ import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.io.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.platform.util.progress.withProgressText import com.intellij.ui.* -import com.intellij.ui.components.JBList import com.intellij.ui.components.panels.OpaquePanel import com.intellij.ui.components.textFieldWithBrowseButton -import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.panel import com.intellij.ui.popup.list.ComboBoxPopup import com.intellij.util.Consumer import com.intellij.util.IconUtil -import com.intellij.util.download.DownloadableFileService -import com.intellij.util.system.CpuArch -import com.intellij.util.text.SemVer 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.async -import kotlinx.coroutines.withContext -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.decodeFromStream import java.awt.BorderLayout import java.awt.Component -import java.nio.file.Path import java.util.* import javax.accessibility.AccessibleContext import javax.swing.DefaultComboBoxModel import javax.swing.JComponent import javax.swing.JList -import javax.swing.JPanel import javax.swing.ListCellRenderer import javax.swing.border.Border -import javax.swing.event.DocumentEvent import javax.swing.tree.DefaultTreeModel class ZigToolchainListEditor() : MasterDetailsComponent() { @@ -187,7 +173,7 @@ class ZigToolchainListEditor() : MasterDetailsComponent() { override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") - private fun addToolchain(uuid: UUID, toolchain: AbstractZigToolchain) { + private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { val node = MyNode(toolchain.createNamedConfigurable(uuid, ProjectManager.getInstance().defaultProject)) addNode(node, myRoot) } @@ -210,7 +196,7 @@ private sealed interface TCListElemIn private sealed interface TCListElem : TCListElemIn { @JvmRecord - data class Toolchain(val toolchain: AbstractZigToolchain) : TCListElem + data class Toolchain(val toolchain: ZigToolchain) : TCListElem object Download : TCListElem object FromDisk : TCListElem } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index b38bca87..b53a4a0e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -22,8 +22,10 @@ package com.falsepattern.zigbrains.project.toolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.resolve +import com.falsepattern.zigbrains.project.toolchain.base.toRef import com.intellij.openapi.components.* -import com.intellij.util.xmlb.annotations.MapAnnotation import java.util.UUID @Service(Service.Level.APP) @@ -32,16 +34,16 @@ import java.util.UUID storages = [Storage("zigbrains.xml")] ) class ZigToolchainListService: SerializablePersistentStateComponent(State()) { - fun setToolchain(uuid: UUID, toolchain: AbstractZigToolchain) { + fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { updateState { - val newMap = HashMap() + val newMap = HashMap() newMap.putAll(it.toolchains) newMap.put(uuid.toString(), toolchain.toRef()) it.copy(toolchains = newMap) } } - fun getToolchain(uuid: UUID): AbstractZigToolchain? { + fun getToolchain(uuid: UUID): ZigToolchain? { return state.toolchains[uuid.toString()]?.resolve() } @@ -52,7 +54,7 @@ class ZigToolchainListService: SerializablePersistentStateComponent> + val toolchains: Sequence> get() = state.toolchains .asSequence() .mapNotNull { @@ -63,7 +65,7 @@ class ZigToolchainListService: SerializablePersistentStateComponent = emptyMap(), + val toolchains: Map = emptyMap(), ) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt similarity index 89% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt index a388e64d..1d381947 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/AbstractZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -20,20 +20,16 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project -import com.intellij.util.xmlb.Converter import com.intellij.util.xmlb.annotations.Attribute import com.intellij.util.xmlb.annotations.MapAnnotation -import com.intellij.util.xmlb.annotations.OptionTag import java.nio.file.Path -import java.util.UUID - -abstract class AbstractZigToolchain { +abstract class ZigToolchain { val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) } abstract fun workingDirectory(project: Project? = null): Path? diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt similarity index 64% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt index 7c4ded3b..9cf1dee4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -20,58 +20,52 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.base import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.UserDataHolder import com.intellij.ui.SimpleColoredComponent -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMapMerge -import kotlinx.coroutines.flow.flattenConcat -import kotlinx.coroutines.flow.map import java.util.UUID private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") -sealed interface ZigToolchainProvider { - suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain? +internal interface ZigToolchainProvider { + suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): ZigToolchain? val serialMarker: String - fun isCompatible(toolchain: AbstractZigToolchain): Boolean - fun deserialize(data: Map): AbstractZigToolchain? - fun serialize(toolchain: AbstractZigToolchain): Map - fun matchesSuggestion(toolchain: AbstractZigToolchain, suggestion: AbstractZigToolchain): Boolean - fun createConfigurable(uuid: UUID, toolchain: AbstractZigToolchain, project: Project): NamedConfigurable - fun suggestToolchains(): List - fun render(toolchain: AbstractZigToolchain, component: SimpleColoredComponent) + fun isCompatible(toolchain: ZigToolchain): Boolean + fun deserialize(data: Map): ZigToolchain? + fun serialize(toolchain: ZigToolchain): Map + fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean + fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, project: Project): NamedConfigurable + fun suggestToolchains(): List + fun render(toolchain: ZigToolchain, component: SimpleColoredComponent) } -fun AbstractZigToolchain.Ref.resolve(): AbstractZigToolchain? { +fun ZigToolchain.Ref.resolve(): ZigToolchain? { val marker = this.marker ?: return null val data = this.data ?: return null val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null return provider.deserialize(data) } -fun AbstractZigToolchain.toRef(): AbstractZigToolchain.Ref { +fun ZigToolchain.toRef(): ZigToolchain.Ref { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return AbstractZigToolchain.Ref(provider.serialMarker, provider.serialize(this)) + return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this)) } -suspend fun Project?.suggestZigToolchain(extraData: UserDataHolder): AbstractZigToolchain? { +suspend fun Project?.suggestZigToolchain(extraData: UserDataHolder): ZigToolchain? { return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(this, extraData) } } -fun AbstractZigToolchain.createNamedConfigurable(uuid: UUID, project: Project): NamedConfigurable { +fun ZigToolchain.createNamedConfigurable(uuid: UUID, project: Project): NamedConfigurable { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() return provider.createConfigurable(uuid, this, project) } -fun suggestZigToolchains(existing: List): List { +fun suggestZigToolchains(existing: List): List { return EXTENSION_POINT_NAME.extensionList.flatMap { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } val suggestions = ext.suggestToolchains() @@ -79,7 +73,7 @@ fun suggestZigToolchains(existing: List): List. */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project @@ -32,12 +33,9 @@ import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull -import kotlinx.coroutines.runBlocking import java.nio.file.Path -import java.util.UUID -import kotlin.io.path.pathString -data class LocalZigToolchain(val location: Path, val std: Path? = null, val name: String? = null): AbstractZigToolchain() { +data class LocalZigToolchain(val location: Path, val std: Path? = null, val name: String? = null): ZigToolchain() { override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } @@ -58,7 +56,7 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, val name val DIRENV_KEY = KeyWithDefaultValue.create("ZIG_LOCAL_DIRENV") @Throws(ExecutionException::class) - fun ensureLocal(toolchain: AbstractZigToolchain): LocalZigToolchain { + fun ensureLocal(toolchain: ZigToolchain): LocalZigToolchain { if (toolchain is LocalZigToolchain) { return toolchain } else { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt similarity index 95% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt index 3cfb420d..2ceb6618 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainConfigurable.kt @@ -20,8 +20,9 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.local +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.NlsContexts diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt similarity index 99% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt index 4cea024c..d486a7f3 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt @@ -20,7 +20,7 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.shared.coroutine.withEDTContext diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt similarity index 81% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index d83e8419..5d63ee66 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -20,14 +20,14 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.direnv.emptyEnv import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.Sdk -import com.intellij.openapi.roots.ui.configuration.SdkPopupBuilder import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.io.FileUtil @@ -36,12 +36,6 @@ import com.intellij.openapi.util.text.StringUtil import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes import com.intellij.util.EnvironmentUtil -import com.intellij.util.IconUtil -import com.intellij.util.system.OS -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.serialization.json.* import java.io.File import java.util.UUID import kotlin.io.path.pathString @@ -60,18 +54,18 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override val serialMarker: String get() = "local" - override fun deserialize(data: Map): AbstractZigToolchain? { + override fun deserialize(data: Map): ZigToolchain? { val location = data["location"]?.toNioPathOrNull() ?: return null val std = data["std"]?.toNioPathOrNull() val name = data["name"] return LocalZigToolchain(location, std, name) } - override fun isCompatible(toolchain: AbstractZigToolchain): Boolean { + override fun isCompatible(toolchain: ZigToolchain): Boolean { return toolchain is LocalZigToolchain } - override fun serialize(toolchain: AbstractZigToolchain): Map { + override fun serialize(toolchain: ZigToolchain): Map { toolchain as LocalZigToolchain val map = HashMap() toolchain.location.pathString.let { map["location"] = it } @@ -81,8 +75,8 @@ class LocalZigToolchainProvider: ZigToolchainProvider { } override fun matchesSuggestion( - toolchain: AbstractZigToolchain, - suggestion: AbstractZigToolchain + toolchain: ZigToolchain, + suggestion: ZigToolchain ): Boolean { toolchain as LocalZigToolchain suggestion as LocalZigToolchain @@ -91,20 +85,20 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override fun createConfigurable( uuid: UUID, - toolchain: AbstractZigToolchain, + toolchain: ZigToolchain, project: Project ): NamedConfigurable { toolchain as LocalZigToolchain return LocalZigToolchainConfigurable(uuid, toolchain, project) } - override fun suggestToolchains(): List { + override fun suggestToolchains(): List { val res = HashSet() EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) } } - override fun render(toolchain: AbstractZigToolchain, component: SimpleColoredComponent) { + override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent) { toolchain as LocalZigToolchain component.append(presentDetectedPath(toolchain.location.pathString)) if (toolchain.name != null) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt index 1f67b4a7..82bcc4d7 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt @@ -22,15 +22,14 @@ package com.falsepattern.zigbrains.project.toolchain.tools -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSerializable +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.openapi.project.Project import kotlinx.coroutines.runBlocking import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import java.nio.file.Path -class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) { +class ZigCompilerTool(toolchain: ZigToolchain) : ZigTool(toolchain) { override val toolName: String get() = "zig" diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt index c7a1e507..fa9c5d49 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.tools -import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.cli.call import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe import com.intellij.execution.configurations.GeneralCommandLine @@ -31,7 +31,7 @@ import com.intellij.openapi.project.Project import java.nio.file.Path import kotlin.io.path.isRegularFile -abstract class ZigTool(val toolchain: AbstractZigToolchain) { +abstract class ZigTool(val toolchain: ZigToolchain) { abstract val toolName: String suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainEnvironmentSerializable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigToolchainEnvironmentSerializable.kt similarity index 93% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainEnvironmentSerializable.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigToolchainEnvironmentSerializable.kt index 8bda26d3..15e8a7ae 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainEnvironmentSerializable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigToolchainEnvironmentSerializable.kt @@ -19,15 +19,16 @@ * You should have received a copy of the GNU Lesser General Public License * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain +package com.falsepattern.zigbrains.project.toolchain.tools + +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.toNioPathOrNull import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import java.nio.file.Path - @JvmRecord @Serializable data class ZigToolchainEnvironmentSerializable( @@ -49,4 +50,4 @@ data class ZigToolchainEnvironmentSerializable( return null } -} +} \ No newline at end of file diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index b5ec5fe5..7d1247b8 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -164,7 +164,7 @@ /> @@ -183,7 +183,7 @@ . */ -package com.falsepattern.zigbrains.project.toolchain +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.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project @@ -32,7 +33,6 @@ 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 diff --git a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml index d07aaa59..d4b6c582 100644 --- a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml +++ b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml @@ -56,7 +56,7 @@ implementation="com.falsepattern.zigbrains.lsp.settings.ZLSSettingsConfigProvider" /> From 8bb4e8bef10a0dd755113c9f332246e90537fef8 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Sun, 6 Apr 2025 17:19:26 +0200 Subject: [PATCH 05/22] project-level toolchain selector --- .../toolchain/ZigToolchainListEditor.kt | 332 ------------------ .../toolchain/ZigToolchainListService.kt | 7 +- .../project/toolchain/ZigToolchainService.kt | 63 ++++ .../project/toolchain/ZigVersionInfo.kt | 106 ------ .../project/toolchain/base/ZigToolchain.kt | 5 +- .../base/ZigToolchainConfigurable.kt | 86 +++++ .../toolchain/base/ZigToolchainPanel.kt | 47 +++ .../toolchain/base/ZigToolchainProvider.kt | 10 +- .../toolchain/downloader/ZigVersionInfo.kt | 115 ++++++ .../toolchain/local/LocalZigToolchain.kt | 2 +- .../local/LocalZigToolchainConfigurable.kt | 71 +--- .../toolchain/local/LocalZigToolchainPanel.kt | 35 +- .../local/LocalZigToolchainProvider.kt | 8 +- .../project/toolchain/ui/Downloader.kt | 87 +++++ .../toolchain/ui/ZigToolchainEditor.kt | 134 +++++++ .../toolchain/ui/ZigToolchainListEditor.kt | 123 +++++++ .../project/toolchain/ui/elements.kt | 55 +++ .../zigbrains/project/toolchain/ui/model.kt | 215 ++++++++++++ .../project/toolchain/ui/popup/popup.kt | 49 +++ .../resources/META-INF/zigbrains-core.xml | 4 +- 20 files changed, 1016 insertions(+), 538 deletions(-) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt deleted file mode 100644 index fb917897..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListEditor.kt +++ /dev/null @@ -1,332 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain - -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.createNamedConfigurable -import com.falsepattern.zigbrains.project.toolchain.base.render -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.icons.AllIcons -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.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.ui.DialogBuilder -import com.intellij.openapi.ui.MasterDetailsComponent -import com.intellij.openapi.util.Disposer -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.* -import com.intellij.ui.components.panels.OpaquePanel -import com.intellij.ui.components.textFieldWithBrowseButton -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.Cell -import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.popup.list.ComboBoxPopup -import com.intellij.util.Consumer -import com.intellij.util.IconUtil -import com.intellij.util.ui.EmptyIcon -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import kotlinx.coroutines.async -import java.awt.BorderLayout -import java.awt.Component -import java.util.* -import javax.accessibility.AccessibleContext -import javax.swing.DefaultComboBoxModel -import javax.swing.JComponent -import javax.swing.JList -import javax.swing.ListCellRenderer -import javax.swing.border.Border -import javax.swing.tree.DefaultTreeModel - -class ZigToolchainListEditor() : MasterDetailsComponent() { - private var isTreeInitialized = false - private var myComponent: JComponent? = null - - override fun createComponent(): JComponent { - if (!isTreeInitialized) { - initTree() - isTreeInitialized = true - } - val comp = super.createComponent() - myComponent = comp - return comp - } - - override fun createActions(fromPopup: Boolean): List { - val add = object : DumbAwareAction({ "lmaoo" }, Presentation.NULL_STRING, IconUtil.addIcon) { - override fun actionPerformed(e: AnActionEvent) { - val toolchains = suggestZigToolchains(zigToolchainList.toolchains.map { it.second }.toList()) - val final = ArrayList() - final.add(TCListElem.Download) - final.add(TCListElem.FromDisk) - final.add(Separator("Detected toolchains", true)) - final.addAll(toolchains.map { TCListElem.Toolchain(it) }) - val model = TCModel(final) - val context = TCContext(null, model) - val popup = TCPopup(context, null, ::onItemSelected) - popup.showInBestPositionFor(e.dataContext) - } - } - return listOf(add, MyDeleteAction()) - } - - override fun onItemDeleted(item: Any?) { - if (item is UUID) { - zigToolchainList.removeToolchain(item) - } - super.onItemDeleted(item) - } - - private fun onItemSelected(elem: TCListElem) { - when (elem) { - is TCListElem.Toolchain -> { - val uuid = UUID.randomUUID() - zigToolchainList.setToolchain(uuid, elem.toolchain) - addToolchain(uuid, elem.toolchain) - (myTree.model as DefaultTreeModel).reload() - } - - is TCListElem.Download -> { - zigCoroutineScope.async { - withEDTContext(ModalityState.stateForComponent(myComponent!!)) { - val info = withModalProgress(myComponent?.let { ModalTaskOwner.component(it) } ?: ModalTaskOwner.guess(), "Fetching zig version information", TaskCancellation.cancellable()) { - ZigVersionInfo.download() - } - val dialog = DialogBuilder() - val theList = ComboBox(DefaultComboBoxModel(info.map { it.first }.toTypedArray())) - val outputPath = textFieldWithBrowseButton( - null, - FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) - ).also { - Disposer.register(dialog, it) - } - var archiveSizeCell: Cell<*>? = null - fun detect(item: String) { - outputPath.text = System.getProperty("user.home") + "/.zig/" + item - val data = info.firstOrNull { it.first == item } ?: return - val size = data.second.dist.size - val sizeMb = size / (1024f * 1024f) - archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) - } - theList.addItemListener { - detect(it.item as String) - } - val center = panel { - row("Version:") { - cell(theList).resizableColumn().align(AlignX.FILL) - } - row("Location:") { - cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } - } - } - detect(info[0].first) - dialog.centerPanel(center) - dialog.setTitle("Version Selector") - dialog.addCancelAction() - dialog.showAndGet() - } - } - } - - is TCListElem.FromDisk -> {} - } - } - - override fun reset() { - reloadTree() - super.reset() - } - - override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchains.empty") - - override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") - - private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { - val node = MyNode(toolchain.createNamedConfigurable(uuid, ProjectManager.getInstance().defaultProject)) - addNode(node, myRoot) - } - - private fun reloadTree() { - myRoot.removeAllChildren() - zigToolchainList.toolchains.forEach { (uuid, toolchain) -> - addToolchain(uuid, toolchain) - } - (myTree.model as DefaultTreeModel).reload() - } - - override fun disposeUIResources() { - super.disposeUIResources() - myComponent = null - } -} - -private sealed interface TCListElemIn - -private sealed interface TCListElem : TCListElemIn { - @JvmRecord - data class Toolchain(val toolchain: ZigToolchain) : TCListElem - object Download : TCListElem - object FromDisk : TCListElem -} - -@JvmRecord -private data class Separator(val text: String, val separatorBar: Boolean) : TCListElemIn - -private class TCPopup( - context: TCContext, - selected: TCListElem?, - onItemSelected: Consumer, -) : ComboBoxPopup(context, selected, onItemSelected) - -private class TCModel private constructor(elements: List, private val separators: Map) : CollectionListModel(elements) { - companion object { - operator fun invoke(input: List): TCModel { - val separators = IdentityHashMap() - var lastSeparator: Separator? = null - val elements = ArrayList() - input.forEach { - when (it) { - is TCListElem -> { - if (lastSeparator != null) { - separators[it] = lastSeparator - lastSeparator = null - } - elements.add(it) - } - - is Separator -> lastSeparator = it - } - } - val model = TCModel(elements, separators) - return model - } - } - - fun separatorAbove(elem: TCListElem) = separators[elem] -} - -private class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { - override fun getProject(): Project? { - return project - } - - override fun getModel(): TCModel { - return model - } - - override fun getRenderer(): ListCellRenderer { - return TCCellRenderer(::getModel) - } -} - -private class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer() { - - override fun getListCellRendererComponent( - list: JList?, - value: TCListElem?, - index: Int, - selected: Boolean, - hasFocus: Boolean - ): Component? { - val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent - val panel = object : CellRendererPanel(BorderLayout()) { - val myContext = component.accessibleContext - - override fun getAccessibleContext(): AccessibleContext? { - return myContext - } - - override fun setBorder(border: Border?) { - component.border = border - } - } - panel.add(component, BorderLayout.CENTER) - - component.isOpaque = true - list?.let { background = if (selected) it.selectionBackground else it.background } - - val model = getModel() - - val separator = value?.let { model.separatorAbove(it) } - - if (separator != null) { - val separatorText = separator.text - val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 - val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) - separatorComponent.isHideLine = !separator.separatorBar - if (separatorText.isNotBlank()) { - separatorComponent.caption = separatorText - } - - val wrapper = OpaquePanel(BorderLayout()) - wrapper.add(separatorComponent, BorderLayout.CENTER) - list?.let { wrapper.background = it.background } - panel.add(wrapper, BorderLayout.NORTH) - } - - return panel - } - - override fun customizeCellRenderer( - list: JList, - value: TCListElem?, - index: Int, - selected: Boolean, - hasFocus: Boolean - ) { - icon = EMPTY_ICON - when (value) { - is TCListElem.Toolchain -> { - icon = Icons.Zig - val toolchain = value.toolchain - toolchain.render(this) - } - - is TCListElem.Download -> { - icon = AllIcons.Actions.Download - append("Download Zig\u2026") - } - - is TCListElem.FromDisk -> { - icon = AllIcons.General.OpenDisk - append("Add Zig from disk\u2026") - } - - null -> {} - } - } -} - -private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index b53a4a0e..d12c54ce 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -67,6 +67,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent = emptyMap(), ) -} -val zigToolchainList get() = service() \ No newline at end of file + companion object { + @JvmStatic + fun getInstance(): ZigToolchainListService = service() + } +} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt new file mode 100644 index 00000000..e08ea5d6 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -0,0 +1,63 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain + +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.intellij.openapi.components.SerializablePersistentStateComponent +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute +import java.util.UUID + +@Service(Service.Level.PROJECT) +@State( + name = "ZigToolchain", + storages = [Storage("zigbrains.xml")] +) +class ZigToolchainService: SerializablePersistentStateComponent(State()) { + var toolchainUUID: UUID? + get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) } + set(value) { + updateState { + it.copy(toolchain = value?.toString() ?: "") + } + } + + val toolchain: ZigToolchain? + get() = toolchainUUID?.let { ZigToolchainListService.getInstance().getToolchain(it) } + + @JvmRecord + data class State( + @JvmField + @Attribute + val toolchain: String = "" + ) + + companion object { + @JvmStatic + fun getInstance(project: Project): ZigToolchainService = project.service() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt deleted file mode 100644 index dcfc7758..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigVersionInfo.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain - -import com.intellij.openapi.application.PathManager -import com.intellij.openapi.progress.coroutineToIndicator -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.platform.util.progress.withProgressText -import com.intellij.util.asSafely -import com.intellij.util.download.DownloadableFileService -import com.intellij.util.system.CpuArch -import com.intellij.util.system.OS -import com.intellij.util.text.SemVer -import com.jetbrains.rd.util.firstOrNull -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import kotlinx.serialization.json.decodeFromJsonElement -import kotlinx.serialization.json.decodeFromStream -import java.nio.file.Path - -@JvmRecord -data class ZigVersionInfo(val date: String, val docs: String, val notes: String, val src: Tarball?, val dist: Tarball) { - companion object { - @OptIn(ExperimentalSerializationApi::class) - suspend fun download(): List> { - return withProgressText("Fetching zig version information") { - withContext(Dispatchers.IO) { - val service = DownloadableFileService.getInstance() - val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json") - val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") - val downloadDirectory = tempPluginDir.toFile() - val downloadResults = coroutineToIndicator { - downloader.download(downloadDirectory) - } - var info: JsonObject? = null - for (result in downloadResults) { - if (result.second.defaultFileName == "index.json") { - info = result.first.inputStream().use { Json.decodeFromStream(it) } - } - } - info?.mapNotNull getVersions@{ (version, data) -> - data as? JsonObject ?: return@getVersions null - val date = data["date"]?.asSafely()?.content ?: "" - val docs = data["docs"]?.asSafely()?.content ?: "" - val notes = data["notes"]?.asSafely()?.content ?: "" - val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } - val dist = data.firstNotNullOfOrNull findCompatible@{ (dist, tb) -> - if (!dist.contains('-')) - return@findCompatible null - val (arch, os) = dist.split('-', limit = 2) - val theArch = when(arch) { - "x86_64" -> CpuArch.X86_64 - "i386" -> CpuArch.X86 - "armv7a" -> CpuArch.ARM32 - "aarch64" -> CpuArch.ARM64 - else -> return@findCompatible null - } - val theOS = when(os) { - "linux" -> OS.Linux - "windows" -> OS.Windows - "macos" -> OS.macOS - "freebsd" -> OS.FreeBSD - else -> return@findCompatible null - } - if (theArch == CpuArch.CURRENT && theOS == OS.CURRENT) { - Json.decodeFromJsonElement(tb) - } else null - } ?: return@getVersions null - Pair(version, ZigVersionInfo(date, docs, notes, src, dist)) - } ?.toList() ?: emptyList() - } - } - } - } -} - -@JvmRecord -@Serializable -data class Tarball(val tarball: String, val shasum: String, val size: Int) - -private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains") \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt index 1d381947..e3cdfc2d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -25,13 +25,16 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project +import com.intellij.openapi.util.UserDataHolderBase import com.intellij.util.xmlb.annotations.Attribute import com.intellij.util.xmlb.annotations.MapAnnotation import java.nio.file.Path -abstract class ZigToolchain { +abstract class ZigToolchain: UserDataHolderBase() { val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) } + abstract val name: String? + abstract fun workingDirectory(project: Project? = null): Path? abstract suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine 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 new file mode 100644 index 00000000..a125fe72 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -0,0 +1,86 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.dsl.builder.panel +import java.util.UUID +import javax.swing.JComponent + +abstract class ZigToolchainConfigurable( + val uuid: UUID, + tc: T +): NamedConfigurable() { + var toolchain: T = tc + set(value) { + ZigToolchainListService.getInstance().setToolchain(uuid, value) + field = value + } + private var myView: ZigToolchainPanel? = null + + abstract fun createPanel(): ZigToolchainPanel + + override fun createOptionsPanel(): JComponent? { + var view = myView + if (view == null) { + view = createPanel() + view.reset(toolchain) + myView = view + } + return panel { + view.attach(this) + } + } + + override fun getEditableObject(): UUID? { + return uuid + } + + override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { + return displayName + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return toolchain.name + } + + override fun isModified(): Boolean { + return myView?.isModified(toolchain) == true + } + + override fun apply() { + myView?.apply(toolchain)?.let { toolchain = it } + } + + override fun reset() { + myView?.reset(toolchain) + } + + override fun disposeUIResources() { + myView?.dispose() + myView = null + super.disposeUIResources() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt new file mode 100644 index 00000000..ab747375 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt @@ -0,0 +1,47 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.intellij.openapi.Disposable +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel + +abstract class ZigToolchainPanel: Disposable { + private val nameField = JBTextField() + + protected var nameFieldValue: String? + get() = nameField.text.ifBlank { null } + set(value) {nameField.text = value ?: ""} + + open fun attach(p: Panel): Unit = with(p) { + row("Name") { + cell(nameField).resizableColumn().align(AlignX.FILL) + } + separator() + } + + abstract fun isModified(toolchain: T): Boolean + abstract fun apply(toolchain: T): T? + abstract fun reset(toolchain: T) +} \ 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 9cf1dee4..ec1362bd 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable @@ -39,7 +40,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, project: Project): NamedConfigurable + fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> fun suggestToolchains(): List fun render(toolchain: ZigToolchain, component: SimpleColoredComponent) } @@ -60,12 +61,13 @@ suspend fun Project?.suggestZigToolchain(extraData: UserDataHolder): ZigToolchai return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(this, extraData) } } -fun ZigToolchain.createNamedConfigurable(uuid: UUID, project: Project): NamedConfigurable { +fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*> { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return provider.createConfigurable(uuid, this, project) + return provider.createConfigurable(uuid, this) } -fun suggestZigToolchains(existing: List): List { +fun suggestZigToolchains(): List { + val existing = ZigToolchainListService.getInstance().toolchains.map { (uuid, tc) -> tc }.toList() return EXTENSION_POINT_NAME.extensionList.flatMap { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } val suggestions = ext.suggestToolchains() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt new file mode 100644 index 00000000..0a2c4311 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt @@ -0,0 +1,115 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.downloader + +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.progress.coroutineToIndicator +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.platform.util.progress.withProgressText +import com.intellij.util.asSafely +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.system.CpuArch +import com.intellij.util.system.OS +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.decodeFromStream +import java.nio.file.Path + +@JvmRecord +data class ZigVersionInfo(val date: String, val docs: String, val notes: String, val src: Tarball?, val dist: Tarball) { + companion object { + suspend fun download(): List> { + return withProgressText("Fetching zig version information") { + withContext(Dispatchers.IO) { + doDownload() + } + } + } + + @OptIn(ExperimentalSerializationApi::class) + private suspend fun doDownload(): List> { + val service = DownloadableFileService.getInstance() + val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json") + val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") + val downloadDirectory = tempPluginDir.toFile() + val downloadResults = coroutineToIndicator { + downloader.download(downloadDirectory) + } + var info: JsonObject? = null + for (result in downloadResults) { + if (result.second.defaultFileName == "index.json") { + info = result.first.inputStream().use { Json.decodeFromStream(it) } + } + } + return info?.mapNotNull { (version, data) -> parseVersion(data)?.let { Pair(version, it) } }?.toList() ?: emptyList() + } + + private fun parseVersion(data: JsonElement): ZigVersionInfo? { + data as? JsonObject ?: return null + val date = data["date"]?.asSafely()?.content ?: "" + val docs = data["docs"]?.asSafely()?.content ?: "" + val notes = data["notes"]?.asSafely()?.content ?: "" + val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } + val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } ?: return null + + return ZigVersionInfo(date, docs, notes, src, dist) + } + + private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { + if (!dist.contains('-')) + return null + val (arch, os) = dist.split('-', limit = 2) + val theArch = when (arch) { + "x86_64" -> CpuArch.X86_64 + "i386" -> CpuArch.X86 + "armv7a" -> CpuArch.ARM32 + "aarch64" -> CpuArch.ARM64 + else -> return null + } + val theOS = when (os) { + "linux" -> OS.Linux + "windows" -> OS.Windows + "macos" -> OS.macOS + "freebsd" -> OS.FreeBSD + else -> return null + } + if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) { + return null + } + return Json.decodeFromJsonElement(tb) + } + } +} + +@JvmRecord +@Serializable +data class Tarball(val tarball: String, val shasum: String, val size: Int) + +private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains") \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index cbb4d47f..f23cb30b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -35,7 +35,7 @@ import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull import java.nio.file.Path -data class LocalZigToolchain(val location: Path, val std: Path? = null, val name: String? = null): ZigToolchain() { +data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain() { override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } 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 2ceb6618..bc8e5adc 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 @@ -22,75 +22,16 @@ package com.falsepattern.zigbrains.project.toolchain.local -import com.falsepattern.zigbrains.project.toolchain.zigToolchainList -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.NamedConfigurable -import com.intellij.openapi.util.NlsContexts -import com.intellij.ui.dsl.builder.panel -import kotlinx.coroutines.runBlocking +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import java.util.UUID -import javax.swing.JComponent class LocalZigToolchainConfigurable( - val uuid: UUID, - toolchain: LocalZigToolchain, - private val project: Project -): NamedConfigurable() { - var toolchain: LocalZigToolchain = toolchain - set(value) { - zigToolchainList.setToolchain(uuid, value) - field = value - } - private var myView: LocalZigToolchainPanel? = null + uuid: UUID, + toolchain: LocalZigToolchain +): ZigToolchainConfigurable(uuid, toolchain) { + override fun createPanel() = LocalZigToolchainPanel() + override fun setDisplayName(name: String?) { toolchain = toolchain.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 = LocalZigToolchainPanel() - view.reset(this) - myView = view - } - return panel { - view.attach(this) - } - } - - override fun getDisplayName(): @NlsContexts.ConfigurableName String? { - var theName = toolchain.name - if (theName == null) { - val version = toolchain.zig.let { runBlocking { it.getEnv(project) } }.getOrNull()?.version - if (version != null) { - theName = "Zig $version" - toolchain = toolchain.copy(name = theName) - } - } - return theName - } - - override fun isModified(): Boolean { - return myView?.isModified(this) == true - } - - override fun apply() { - myView?.apply(this) - } - - override fun reset() { - myView?.reset(this) - } - - override fun disposeUIResources() { - super.disposeUIResources() - } } \ No newline at end of file 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 d486a7f3..d2301d35 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt @@ -23,6 +23,7 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainPanel import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.Disposable @@ -45,8 +46,7 @@ import kotlinx.coroutines.launch import javax.swing.event.DocumentEvent import kotlin.io.path.pathString -class LocalZigToolchainPanel() : Disposable { - private val nameField = JBTextField() +class LocalZigToolchainPanel() : ZigToolchainPanel() { private val pathToToolchain = textFieldWithBrowseButton( null, FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) @@ -75,11 +75,8 @@ class LocalZigToolchainPanel() : Disposable { ).also { Disposer.register(this, it) } private var debounce: Job? = null - fun attach(p: Panel): Unit = with(p) { - row("Name") { - cell(nameField).resizableColumn().align(AlignX.FILL) - } - separator() + override fun attach(p: Panel): Unit = with(p) { + super.attach(p) row(ZigBrainsBundle.message("settings.project.label.toolchain")) { cell(pathToToolchain).resizableColumn().align(AlignX.FILL) } @@ -92,27 +89,23 @@ class LocalZigToolchainPanel() : Disposable { } } - fun isModified(cfg: LocalZigToolchainConfigurable): Boolean { - val name = nameField.text.ifBlank { null } ?: return false - val tc = cfg.toolchain + override fun isModified(toolchain: LocalZigToolchain): Boolean { + val name = nameFieldValue ?: return false val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null - return name != cfg.displayName || tc.location != location || tc.std != std + return name != toolchain.name || toolchain.location != location || toolchain.std != std } - fun apply(cfg: LocalZigToolchainConfigurable): Boolean { - val tc = cfg.toolchain - val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false + override fun apply(toolchain: LocalZigToolchain): LocalZigToolchain? { + val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return null val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null - cfg.toolchain = tc.copy(location = location, std = std, name = nameField.text ?: "") - return true + return toolchain.copy(location = location, std = std, name = nameFieldValue ?: "") } - fun reset(cfg: LocalZigToolchainConfigurable) { - nameField.text = cfg.displayName ?: "" - val tc = cfg.toolchain - this.pathToToolchain.text = tc.location.pathString - val std = tc.std + override fun reset(toolchain: LocalZigToolchain) { + nameFieldValue = toolchain.name + this.pathToToolchain.text = toolchain.location.pathString + val std = toolchain.std if (std != null) { stdFieldOverride.isSelected = true pathToStd.text = std.pathString diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index 5d63ee66..e4761617 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.direnv.emptyEnv import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable @@ -85,11 +86,10 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override fun createConfigurable( uuid: UUID, - toolchain: ZigToolchain, - project: Project - ): NamedConfigurable { + toolchain: ZigToolchain + ): ZigToolchainConfigurable<*> { toolchain as LocalZigToolchain - return LocalZigToolchainConfigurable(uuid, toolchain, project) + return LocalZigToolchainConfigurable(uuid, toolchain) } override fun suggestToolchains(): List { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt new file mode 100644 index 00000000..ba80d0f7 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt @@ -0,0 +1,87 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.util.Disposer +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.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.panel +import java.awt.Component +import javax.swing.DefaultComboBoxModel + +object Downloader { + suspend fun openDownloadDialog(component: Component) { + val info = withModalProgress( + component.let { ModalTaskOwner.component(it) }, + "Fetching zig version information", + TaskCancellation.Companion.cancellable()) { + ZigVersionInfo.Companion.download() + } + withEDTContext(ModalityState.stateForComponent(component)) { + val dialog = DialogBuilder() + val theList = ComboBox(DefaultComboBoxModel(info.map { it.first }.toTypedArray())) + val outputPath = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + ).also { + Disposer.register(dialog, it) + } + var archiveSizeCell: Cell<*>? = null + fun detect(item: String) { + outputPath.text = System.getProperty("user.home") + "/.zig/" + item + val data = info.firstOrNull { it.first == item } ?: return + val size = data.second.dist.size + val sizeMb = size / (1024f * 1024f) + archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) + } + theList.addItemListener { + detect(it.item as String) + } + val center = panel { + row("Version:") { + cell(theList).resizableColumn().align(AlignX.FILL) + } + row("Location:") { + cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } + } + } + detect(info[0].first) + dialog.centerPanel(center) + dialog.setTitle("Version Selector") + dialog.addCancelAction() + dialog.showAndGet() + Disposer.dispose(dialog) + } + } +} \ 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 new file mode 100644 index 00000000..c8de5969 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -0,0 +1,134 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +import com.intellij.openapi.Disposable +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.NlsContexts +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.panel +import java.awt.event.ItemEvent +import javax.swing.JComponent +import kotlin.collections.addAll + +class ZigToolchainEditor(private val project: Project): Configurable { + private var myUi: UI? = null + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return "Zig" + } + + override fun createComponent(): JComponent? { + if (myUi != null) { + disposeUIResources() + } + val ui = UI() + myUi = ui + return panel { + ui.attach(this) + } + } + + override fun isModified(): Boolean { + return myUi?.isModified() == true + } + + override fun apply() { + myUi?.apply() + } + + override fun reset() { + myUi?.reset() + } + + override fun disposeUIResources() { + myUi?.let { Disposer.dispose(it) } + myUi = null + super.disposeUIResources() + } + + inner class UI(): Disposable { + private val toolchainBox: TCComboBox + private var oldSelectionIndex: Int = 0 + init { + val modelList = ArrayList() + modelList.add(TCListElem.None) + modelList.addAll(ZigToolchainListService.Companion.getInstance().toolchains.map { it.asActual() }) + modelList.add(Separator("", true)) + modelList.addAll(TCListElem.fetchGroup) + modelList.add(Separator("Detected toolchains", true)) + modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) + toolchainBox = TCComboBox(TCModel(modelList)) + toolchainBox.addItemListener(::itemStateChanged) + reset() + } + + private fun itemStateChanged(event: ItemEvent) { + if (event.stateChange != ItemEvent.SELECTED) { + return + } + val item = event.item + if (item !is TCListElem) { + toolchainBox.selectedIndex = oldSelectionIndex + return + } + when(item) { + is TCListElem.None, is TCListElem.Toolchain.Actual -> { + oldSelectionIndex = toolchainBox.selectedIndex + } + else -> { + toolchainBox.selectedIndex = oldSelectionIndex + } + } + } + + + fun attach(p: Panel): Unit = with(p) { + row("Toolchain") { + cell(toolchainBox).resizableColumn().align(AlignX.FILL) + } + } + + fun isModified(): Boolean { + return ZigToolchainService.Companion.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain + } + + fun apply() { + ZigToolchainService.Companion.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain + } + + fun reset() { + toolchainBox.selectedToolchain = ZigToolchainService.Companion.getInstance(project).toolchainUUID + oldSelectionIndex = toolchainBox.selectedIndex + } + + override fun dispose() { + + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt new file mode 100644 index 00000000..908acb78 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -0,0 +1,123 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.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.shared.zigCoroutineScope +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.ui.MasterDetailsComponent +import com.intellij.util.IconUtil +import kotlinx.coroutines.async +import java.util.UUID +import javax.swing.JComponent +import javax.swing.tree.DefaultTreeModel + +class ZigToolchainListEditor() : MasterDetailsComponent() { + private var isTreeInitialized = false + private var myComponent: JComponent? = null + + override fun createComponent(): JComponent { + if (!isTreeInitialized) { + initTree() + isTreeInitialized = true + } + val comp = super.createComponent() + myComponent = comp + return comp + } + + override fun createActions(fromPopup: Boolean): List { + val add = object : DumbAwareAction({ "lmaoo" }, Presentation.NULL_STRING, IconUtil.addIcon) { + override fun actionPerformed(e: AnActionEvent) { + val modelList = ArrayList() + modelList.addAll(TCListElem.fetchGroup) + modelList.add(Separator("Detected toolchains", true)) + modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) + val model = TCModel.Companion(modelList) + val context = TCContext(null, model) + val popup = TCComboBoxPopup(context, null, ::onItemSelected) + popup.showInBestPositionFor(e.dataContext) + } + } + return listOf(add, MyDeleteAction()) + } + + override fun onItemDeleted(item: Any?) { + if (item is UUID) { + ZigToolchainListService.getInstance().removeToolchain(item) + } + super.onItemDeleted(item) + } + + private fun onItemSelected(elem: TCListElem) { + when (elem) { + is TCListElem.Toolchain -> { + val uuid = UUID.randomUUID() + ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain) + addToolchain(uuid, elem.toolchain) + (myTree.model as DefaultTreeModel).reload() + } + is TCListElem.Download -> { + zigCoroutineScope.async { + Downloader.openDownloadDialog(myComponent!!) + } + } + is TCListElem.FromDisk -> {} + is TCListElem.None -> {} + } + } + + override fun reset() { + reloadTree() + super.reset() + } + + override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchains.empty") + + override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") + + private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { + val node = MyNode(toolchain.createNamedConfigurable(uuid)) + addNode(node, myRoot) + } + + private fun reloadTree() { + myRoot.removeAllChildren() + ZigToolchainListService.getInstance().toolchains.forEach { (uuid, toolchain) -> + addToolchain(uuid, toolchain) + } + (myTree.model as DefaultTreeModel).reload() + } + + override fun disposeUIResources() { + super.disposeUIResources() + myComponent = null + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt new file mode 100644 index 00000000..45045252 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt @@ -0,0 +1,55 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import java.util.UUID + + +internal sealed interface TCListElemIn + +internal sealed interface TCListElem : TCListElemIn { + sealed interface Toolchain : TCListElem { + val toolchain: ZigToolchain + + @JvmRecord + data class Suggested(override val toolchain: ZigToolchain): Toolchain + + @JvmRecord + data class Actual(val uuid: UUID, override val toolchain: ZigToolchain): Toolchain + } + object None: TCListElem + object Download : TCListElem + object FromDisk : TCListElem + + companion object { + val fetchGroup get() = listOf(Download, FromDisk) + } +} + +@JvmRecord +internal data class Separator(val text: String, val line: Boolean) : TCListElemIn + +internal fun Pair.asActual() = TCListElem.Toolchain.Actual(first, second) + +internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt new file mode 100644 index 00000000..3c834ca4 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -0,0 +1,215 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import ai.grazie.utils.attributes.value +import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.project.toolchain.base.render +import com.intellij.icons.AllIcons +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.ui.EmptyIcon +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import java.awt.BorderLayout +import java.awt.Component +import java.util.IdentityHashMap +import java.util.UUID +import javax.accessibility.AccessibleContext +import javax.swing.JList +import javax.swing.border.Border + +internal class TCComboBoxPopup( + context: TCContext, + selected: TCListElem?, + onItemSelected: Consumer, +) : ComboBoxPopup(context, selected, onItemSelected) + +internal class TCComboBox(model: TCModel): ComboBox(model) { + init { + setRenderer(TCCellRenderer({model})) + } + + var selectedToolchain: UUID? + set(value) { + if (value == null) { + selectedItem = TCListElem.None + return + } + for (i in 0.. item.uuid + else -> null + } + } +} + +internal class TCModel private constructor(elements: List, private val separators: Map) : CollectionComboBoxModel(elements) { + companion object { + operator fun invoke(input: List): TCModel { + val separators = IdentityHashMap() + var lastSeparator: Separator? = null + val elements = ArrayList() + input.forEach { + when (it) { + is TCListElem -> { + if (lastSeparator != null) { + separators[it] = lastSeparator + lastSeparator = null + } + elements.add(it) + } + + is Separator -> lastSeparator = it + } + } + val model = TCModel(elements, separators) + return model + } + } + + fun separatorAbove(elem: TCListElem) = separators[elem] +} + +internal class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { + override fun getProject(): Project? { + return project + } + + override fun getModel(): TCModel { + return model + } + + override fun getRenderer(): TCCellRenderer { + return TCCellRenderer(::getModel) + } +} + +internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer() { + + override fun getListCellRendererComponent( + list: JList?, + value: TCListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ): Component? { + val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent + val panel = object : CellRendererPanel(BorderLayout()) { + val myContext = component.accessibleContext + + override fun getAccessibleContext(): AccessibleContext? { + return myContext + } + + override fun setBorder(border: Border?) { + component.border = border + } + } + panel.add(component, BorderLayout.CENTER) + + component.isOpaque = true + list?.let { background = if (selected) it.selectionBackground else it.background } + + val model = getModel() + + if (index == -1) { + component.isOpaque = false + panel.isOpaque = false + return panel + } + + val separator = value?.let { model.separatorAbove(it) } + + if (separator != null) { + val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 + val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) + separatorComponent.isHideLine = !separator.line + separatorComponent.caption = separator.text.ifBlank { null } + val wrapper = OpaquePanel(BorderLayout()) + wrapper.add(separatorComponent, BorderLayout.CENTER) + list?.let { wrapper.background = it.background } + panel.add(wrapper, BorderLayout.NORTH) + } + + return panel + } + + override fun customizeCellRenderer( + list: JList, + value: TCListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + icon = EMPTY_ICON + when (value) { + is TCListElem.Toolchain -> { + icon = when(value) { + is TCListElem.Toolchain.Suggested -> AllIcons.General.Information + is TCListElem.Toolchain.Actual -> Icons.Zig + } + val toolchain = value.toolchain + toolchain.render(this) + } + + is TCListElem.Download -> { + icon = AllIcons.Actions.Download + append("Download Zig\u2026") + } + + is TCListElem.FromDisk -> { + icon = AllIcons.General.OpenDisk + append("Add Zig from disk\u2026") + } + + is TCListElem.None, null -> { + icon = AllIcons.General.BalloonError + append("", SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } +} + +private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt new file mode 100644 index 00000000..e5742ba5 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt @@ -0,0 +1,49 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui.popup + +import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.project.toolchain.base.render +import com.falsepattern.zigbrains.project.toolchain.ui.Separator +import com.falsepattern.zigbrains.project.toolchain.ui.TCListElem +import com.falsepattern.zigbrains.project.toolchain.ui.TCListElemIn +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.ui.CellRendererPanel +import com.intellij.ui.CollectionListModel +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.Consumer +import com.intellij.util.ui.EmptyIcon +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import java.awt.BorderLayout +import java.awt.Component +import java.util.IdentityHashMap +import javax.accessibility.AccessibleContext +import javax.swing.JList +import javax.swing.border.Border + diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index 7d1247b8..8e51f2ab 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -140,13 +140,13 @@ /> From 90230264787baaeaec5d140b6c3b080fe21e9c51 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Sun, 6 Apr 2025 17:31:10 +0200 Subject: [PATCH 06/22] nicer suggestion/actual toolchain rendering --- .../toolchain/base/ZigToolchainProvider.kt | 6 +++--- .../local/LocalZigToolchainProvider.kt | 19 +++++++++++++++---- .../zigbrains/project/toolchain/ui/model.kt | 9 +++++---- 3 files changed, 23 insertions(+), 11 deletions(-) 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 ec1362bd..43f3df41 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 @@ -42,7 +42,7 @@ internal interface ZigToolchainProvider { fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> fun suggestToolchains(): List - fun render(toolchain: ZigToolchain, component: SimpleColoredComponent) + fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean) } fun ZigToolchain.Ref.resolve(): ZigToolchain? { @@ -75,7 +75,7 @@ fun suggestZigToolchains(): List { } } -fun ZigToolchain.render(component: SimpleColoredComponent) { +fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean) { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return provider.render(this, component) + return provider.render(this, component, isSuggestion) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index e4761617..4fa6412c 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -98,12 +98,23 @@ class LocalZigToolchainProvider: ZigToolchainProvider { return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) } } - override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent) { + override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean) { toolchain as LocalZigToolchain - component.append(presentDetectedPath(toolchain.location.pathString)) - if (toolchain.name != null) { + val path = presentDetectedPath(toolchain.location.pathString) + val name = toolchain.name + val primary: String + val secondary: String? + if (isSuggestion) { + primary = path + secondary = name + } else { + primary = name ?: "Zig" + secondary = path + } + component.append(primary) + if (secondary != null) { component.append(" ") - component.append(toolchain.name, SimpleTextAttributes.GRAYED_ATTRIBUTES) + component.append(secondary, SimpleTextAttributes.GRAYED_ATTRIBUTES) } } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index 3c834ca4..7322e741 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -186,12 +186,13 @@ internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRend icon = EMPTY_ICON when (value) { is TCListElem.Toolchain -> { - icon = when(value) { - is TCListElem.Toolchain.Suggested -> AllIcons.General.Information - is TCListElem.Toolchain.Actual -> Icons.Zig + val (icon, isSuggestion) = when(value) { + is TCListElem.Toolchain.Suggested -> AllIcons.General.Information to true + is TCListElem.Toolchain.Actual -> Icons.Zig to false } + this.icon = icon val toolchain = value.toolchain - toolchain.render(this) + toolchain.render(this, isSuggestion) } is TCListElem.Download -> { From 9676b70821db016541e40ea1d78a3a9685b01a02 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Mon, 7 Apr 2025 02:38:12 +0200 Subject: [PATCH 07/22] Working change sync and downloader --- .../toolchain/ZigDebuggerToolchainService.kt | 35 +-- .../toolchain/ZigToolchainListService.kt | 41 +++- .../toolchain/downloader/ZigVersionInfo.kt | 110 +++++++--- .../project/toolchain/ui/Downloader.kt | 206 +++++++++++++++--- .../toolchain/ui/ZigToolchainEditor.kt | 44 ++-- .../toolchain/ui/ZigToolchainListEditor.kt | 29 ++- .../zigbrains/project/toolchain/ui/model.kt | 17 +- .../zigbrains/shared/Unarchiver.kt | 66 ++++++ .../shared/coroutine/CoroutinesUtil.kt | 23 +- 9 files changed, 437 insertions(+), 134 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt index c6f01bf7..7feadf32 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt @@ -23,6 +23,7 @@ package com.falsepattern.zigbrains.debugger.toolchain import com.falsepattern.zigbrains.debugger.ZigDebugBundle +import com.falsepattern.zigbrains.shared.Unarchiver import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.application.PathManager @@ -40,14 +41,12 @@ import com.intellij.ui.components.JBPanel import com.intellij.util.application import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.download.DownloadableFileService -import com.intellij.util.io.Decompressor import com.intellij.util.system.CpuArch import com.intellij.util.system.OS import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager import com.jetbrains.cidr.execution.debugger.backend.bin.UrlProvider import com.jetbrains.cidr.execution.debugger.backend.lldb.LLDBDriverConfiguration import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import java.io.IOException import java.net.URL @@ -329,38 +328,6 @@ class ZigDebuggerToolchainService { } } - private enum class Unarchiver { - ZIP { - override val extension = "zip" - override fun createDecompressor(file: Path) = Decompressor.Zip(file) - }, - TAR { - override val extension = "tar.gz" - override fun createDecompressor(file: Path) = Decompressor.Tar(file) - }, - VSIX { - override val extension = "vsix" - override fun createDecompressor(file: Path) = Decompressor.Zip(file) - }; - - protected abstract val extension: String - protected abstract fun createDecompressor(file: Path): Decompressor - - companion object { - @Throws(IOException::class) - suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { - runInterruptible { - val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } ?: error("Unexpected archive type: $archivePath") - val dec = unarchiver.createDecompressor(archivePath) - if (prefix != null) { - dec.removePrefixPath(prefix) - } - dec.extract(dst) - } - } - } - } - sealed class DownloadResult { class Ok(val baseDir: Path): DownloadResult() data object NoUrls: DownloadResult() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index d12c54ce..740f551a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.resolve import com.falsepattern.zigbrains.project.toolchain.base.toRef import com.intellij.openapi.components.* +import java.lang.ref.WeakReference import java.util.UUID @Service(Service.Level.APP) @@ -34,13 +35,15 @@ import java.util.UUID storages = [Storage("zigbrains.xml")] ) class ZigToolchainListService: SerializablePersistentStateComponent(State()) { + private val changeListeners = ArrayList>() fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { updateState { val newMap = HashMap() newMap.putAll(it.toolchains) - newMap.put(uuid.toString(), toolchain.toRef()) + newMap[uuid.toString()] = toolchain.toRef() it.copy(toolchains = newMap) } + notifyChanged() } fun getToolchain(uuid: UUID): ZigToolchain? { @@ -52,6 +55,37 @@ class ZigToolchainListService: SerializablePersistentStateComponent> @@ -72,4 +106,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent> { - return withProgressText("Fetching zig version information") { - withContext(Dispatchers.IO) { - doDownload() - } +data class ZigVersionInfo( + val version: SemVer, + val date: String, + val docs: String, + val notes: String, + val src: Tarball?, + val dist: Tarball +) { + suspend fun downloadAndUnpack(into: Path): Boolean { + return reportProgress { reporter -> + try { + into.createDirectories() + } catch (e: Exception) { + return@reportProgress false } - } - - @OptIn(ExperimentalSerializationApi::class) - private suspend fun doDownload(): List> { val service = DownloadableFileService.getInstance() - val desc = service.createFileDescription("https://ziglang.org/download/index.json", "index.json") + val fileName = dist.tarball.substringAfterLast('/') + val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) + val desc = service.createFileDescription(dist.tarball, tempFile.name) val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") - val downloadDirectory = tempPluginDir.toFile() - val downloadResults = coroutineToIndicator { - downloader.download(downloadDirectory) - } - var info: JsonObject? = null - for (result in downloadResults) { - if (result.second.defaultFileName == "index.json") { - info = result.first.inputStream().use { Json.decodeFromStream(it) } + val downloadResults = reporter.sizedStep(100) { + coroutineToIndicator { + downloader.download(into.toFile()) } } - return info?.mapNotNull { (version, data) -> parseVersion(data)?.let { Pair(version, it) } }?.toList() ?: emptyList() + if (downloadResults.isEmpty()) + return@reportProgress false + val tarball = downloadResults[0].first + reporter.indeterminateStep("Extracting tarball") { + Unarchiver.unarchive(tarball.toPath(), into) + tarball.delete() + val contents = Files.newDirectoryStream(into).use { it.toList() } + if (contents.size == 1 && contents[0].isDirectory()) { + val src = contents[0] + Files.newDirectoryStream(src).use { stream -> + stream.forEach { + it.move(into.resolve(src.relativize(it))) + } + } + src.delete() + } + } + return@reportProgress true + } + } + companion object { + @OptIn(ExperimentalSerializationApi::class) + suspend fun downloadVersionList(): List { + val service = DownloadableFileService.getInstance() + val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false) + val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name) + val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") + val downloadResults = coroutineToIndicator { + downloader.download(tempPluginDir) + } + if (downloadResults.isEmpty()) + return emptyList() + val index = downloadResults[0].first + val info = index.inputStream().use { Json.decodeFromStream(it) } + index.delete() + return info.mapNotNull { (version, data) -> parseVersion(version, data) }.toList() } - private fun parseVersion(data: JsonElement): ZigVersionInfo? { - data as? JsonObject ?: return null + private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? { + if (data !is JsonObject) + return null + + val versionTag = data["version"]?.asSafely()?.content + + val version = SemVer.parseFromText(versionTag) ?: SemVer.parseFromText(versionKey) + ?: return null val date = data["date"]?.asSafely()?.content ?: "" val docs = data["docs"]?.asSafely()?.content ?: "" - val notes = data["notes"]?.asSafely()?.content ?: "" + val notes = data["notes"]?.asSafely()?.content?: "" val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } - val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } ?: return null + val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } + ?: return null - return ZigVersionInfo(date, docs, notes, src, dist) + + return ZigVersionInfo(version, date, docs, notes, src, dist) } private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { @@ -112,4 +164,4 @@ data class ZigVersionInfo(val date: String, val docs: String, val notes: String, @Serializable data class Tarball(val tarball: String, val shasum: String, val size: Int) -private val tempPluginDir get(): Path = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains") \ No newline at end of file +private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt index ba80d0f7..f7770eb8 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt @@ -24,64 +24,200 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.intellij.openapi.application.ModalityState +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT +import com.intellij.icons.AllIcons import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.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.ColoredListCellRenderer +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBLabel import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.panel +import com.intellij.util.asSafely +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.awt.Component +import java.nio.file.Files +import java.nio.file.Path +import java.util.UUID import javax.swing.DefaultComboBoxModel +import javax.swing.JList +import javax.swing.event.DocumentEvent +import kotlin.contracts.ExperimentalContracts +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +//TODO lang object Downloader { - suspend fun openDownloadDialog(component: Component) { + suspend fun downloadToolchain(component: Component): UUID? { val info = withModalProgress( - component.let { ModalTaskOwner.component(it) }, + ModalTaskOwner.component(component), "Fetching zig version information", - TaskCancellation.Companion.cancellable()) { - ZigVersionInfo.Companion.download() + TaskCancellation.cancellable()) { + withContext(Dispatchers.IO) { + ZigVersionInfo.downloadVersionList() + } } - withEDTContext(ModalityState.stateForComponent(component)) { - val dialog = DialogBuilder() - val theList = ComboBox(DefaultComboBoxModel(info.map { it.first }.toTypedArray())) - val outputPath = textFieldWithBrowseButton( - null, - FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) - ).also { - Disposer.register(dialog, it) + val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) { + selectToolchain(info) + } ?: return null + withModalProgress( + ModalTaskOwner.component(component), + "Downloading zig tarball", + TaskCancellation.cancellable()) { + withContext(Dispatchers.IO) { + version.downloadAndUnpack(downloadPath) } - var archiveSizeCell: Cell<*>? = null - fun detect(item: String) { - outputPath.text = System.getProperty("user.home") + "/.zig/" + item - val data = info.firstOrNull { it.first == item } ?: return - val size = data.second.dist.size - val sizeMb = size / (1024f * 1024f) - archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) + } + return null + } + + private enum class DirectoryState { + Invalid, + NotAbsolute, + NotDirectory, + NotEmpty, + CreateNew, + Ok; + + fun isValid(): Boolean { + return when(this) { + Invalid, NotAbsolute, NotDirectory, NotEmpty -> false + CreateNew, Ok -> true } - theList.addItemListener { - detect(it.item as String) - } - val center = panel { - row("Version:") { - cell(theList).resizableColumn().align(AlignX.FILL) + } + + companion object { + @OptIn(ExperimentalContracts::class) + @JvmStatic + fun determine(path: Path?): DirectoryState { + if (path == null) { + return Invalid } - row("Location:") { - cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } + if (!path.isAbsolute) { + return NotAbsolute } + if (!path.exists()) { + var parent: Path? = path.parent + while(parent != null) { + if (!parent.exists()) { + parent = parent.parent + continue + } + if (!parent.isDirectory()) { + return NotDirectory + } + return CreateNew + } + return Invalid + } + if (!path.isDirectory()) { + return NotDirectory + } + val isEmpty = Files.newDirectoryStream(path).use { !it.iterator().hasNext() } + if (!isEmpty) { + return NotEmpty + } + return Ok } - detect(info[0].first) - dialog.centerPanel(center) - dialog.setTitle("Version Selector") - dialog.addCancelAction() - dialog.showAndGet() - Disposer.dispose(dialog) } } + + private fun selectToolchain(info: List): Pair? { + val dialog = DialogBuilder() + val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray())) + theList.renderer = object: ColoredListCellRenderer() { + override fun customizeCellRenderer( + list: JList, + value: ZigVersionInfo?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + value?.let { append(it.version.rawVersion) } + } + } + val outputPath = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + ) + Disposer.register(dialog, outputPath) + outputPath.textField.columns = 50 + + lateinit var errorMessageBox: JBLabel + outputPath.addDocumentListener(object: DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() + val state = DirectoryState.determine(path) + if (state.isValid()) { + errorMessageBox.icon = AllIcons.General.Information + dialog.setOkActionEnabled(true) + } else { + errorMessageBox.icon = AllIcons.General.Error + dialog.setOkActionEnabled(false) + } + errorMessageBox.text = when(state) { + DirectoryState.Invalid -> "Invalid path" + DirectoryState.NotAbsolute -> "Must be an absolute path" + DirectoryState.NotDirectory -> "Path is not a directory" + DirectoryState.NotEmpty -> "Directory is not empty" + DirectoryState.CreateNew -> "Directory will be created" + DirectoryState.Ok -> "Directory OK" + } + dialog.window.repaint() + } + }) + var archiveSizeCell: Cell<*>? = null + fun detect(item: ZigVersionInfo) { + outputPath.text = System.getProperty("user.home") + "/.zig/" + item.version + val size = item.dist.size + val sizeMb = size / (1024f * 1024f) + archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) + } + theList.addItemListener { + detect(it.item as ZigVersionInfo) + } + val center = panel { + row("Version:") { + cell(theList).resizableColumn().align(AlignX.FILL) + } + row("Location:") { + cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } + } + row { + errorMessageBox = JBLabel() + cell(errorMessageBox) + } + } + detect(info[0]) + dialog.centerPanel(center) + dialog.setTitle("Version Selector") + dialog.addCancelAction() + dialog.addOkAction().also { it.setText("Download") } + if (!dialog.showAndGet()) { + return null + } + val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() + ?: return null + if (!DirectoryState.determine(path).isValid()) { + return null + } + val version = theList.selectedItem?.asSafely() + ?: return null + + return path to version + } + + private suspend fun installToolchain(path: Path, version: ZigVersionInfo): Boolean { + TODO("Not yet implemented") + } } \ 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 c8de5969..e83c00e1 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 @@ -72,19 +72,15 @@ class ZigToolchainEditor(private val project: Project): Configurable { super.disposeUIResources() } - inner class UI(): Disposable { + inner class UI(): Disposable, ZigToolchainListService.ToolchainListChangeListener { private val toolchainBox: TCComboBox private var oldSelectionIndex: Int = 0 + private val model: TCModel init { - val modelList = ArrayList() - modelList.add(TCListElem.None) - modelList.addAll(ZigToolchainListService.Companion.getInstance().toolchains.map { it.asActual() }) - modelList.add(Separator("", true)) - modelList.addAll(TCListElem.fetchGroup) - modelList.add(Separator("Detected toolchains", true)) - modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) - toolchainBox = TCComboBox(TCModel(modelList)) + model = TCModel(getModelList()) + toolchainBox = TCComboBox(model) toolchainBox.addItemListener(::itemStateChanged) + ZigToolchainListService.getInstance().addChangeListener(this) reset() } @@ -107,6 +103,16 @@ class ZigToolchainEditor(private val project: Project): Configurable { } } + override fun toolchainListChanged() { + val selected = model.selected + val list = getModelList() + model.updateContents(list) + if (selected != null && list.contains(selected)) { + model.selectedItem = selected + } else { + model.selectedItem = TCListElem.None + } + } fun attach(p: Panel): Unit = with(p) { row("Toolchain") { @@ -115,20 +121,32 @@ class ZigToolchainEditor(private val project: Project): Configurable { } fun isModified(): Boolean { - return ZigToolchainService.Companion.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain + return ZigToolchainService.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain } fun apply() { - ZigToolchainService.Companion.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain + ZigToolchainService.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain } fun reset() { - toolchainBox.selectedToolchain = ZigToolchainService.Companion.getInstance(project).toolchainUUID + toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID oldSelectionIndex = toolchainBox.selectedIndex } override fun dispose() { - + ZigToolchainListService.getInstance().removeChangeListener(this) } } +} + + +private fun getModelList(): List { + val modelList = ArrayList() + modelList.add(TCListElem.None) + modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }) + modelList.add(Separator("", true)) + modelList.addAll(TCListElem.fetchGroup) + modelList.add(Separator("Detected toolchains", true)) + modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) + return modelList } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index 908acb78..0d202210 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -27,6 +27,7 @@ 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.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -34,23 +35,25 @@ import com.intellij.openapi.actionSystem.Presentation import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.util.IconUtil -import kotlinx.coroutines.async +import kotlinx.coroutines.launch import java.util.UUID import javax.swing.JComponent import javax.swing.tree.DefaultTreeModel -class ZigToolchainListEditor() : MasterDetailsComponent() { +class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener { private var isTreeInitialized = false - private var myComponent: JComponent? = null + private var registered: Boolean = false override fun createComponent(): JComponent { if (!isTreeInitialized) { initTree() isTreeInitialized = true } - val comp = super.createComponent() - myComponent = comp - return comp + if (!registered) { + ZigToolchainListService.getInstance().addChangeListener(this) + registered = true + } + return super.createComponent() } override fun createActions(fromPopup: Boolean): List { @@ -81,12 +84,10 @@ class ZigToolchainListEditor() : MasterDetailsComponent() { is TCListElem.Toolchain -> { val uuid = UUID.randomUUID() ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain) - addToolchain(uuid, elem.toolchain) - (myTree.model as DefaultTreeModel).reload() } is TCListElem.Download -> { - zigCoroutineScope.async { - Downloader.openDownloadDialog(myComponent!!) + zigCoroutineScope.launch(myWholePanel.asContextElement()) { + Downloader.downloadToolchain(myWholePanel) } } is TCListElem.FromDisk -> {} @@ -118,6 +119,12 @@ class ZigToolchainListEditor() : MasterDetailsComponent() { override fun disposeUIResources() { super.disposeUIResources() - myComponent = null + if (registered) { + ZigToolchainListService.getInstance().removeChangeListener(this) + } + } + + override fun toolchainListChanged() { + reloadTree() } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index 7322e741..e3a56704 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -85,9 +85,15 @@ internal class TCComboBox(model: TCModel): ComboBox(model) { } } -internal class TCModel private constructor(elements: List, private val separators: Map) : CollectionComboBoxModel(elements) { +internal class TCModel private constructor(elements: List, private var separators: Map) : CollectionComboBoxModel(elements) { companion object { operator fun invoke(input: List): TCModel { + val (elements, separators) = convert(input) + val model = TCModel(elements, separators) + return model + } + + private fun convert(input: List): Pair, Map> { val separators = IdentityHashMap() var lastSeparator: Separator? = null val elements = ArrayList() @@ -104,12 +110,17 @@ internal class TCModel private constructor(elements: List, private v is Separator -> lastSeparator = it } } - val model = TCModel(elements, separators) - return model + return elements to separators } } fun separatorAbove(elem: TCListElem) = separators[elem] + + fun updateContents(input: List) { + val (elements, separators) = convert(input) + this.separators = separators + replaceAll(elements) + } } internal class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt new file mode 100644 index 00000000..578567f1 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt @@ -0,0 +1,66 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +import com.intellij.util.io.Decompressor +import kotlinx.coroutines.runInterruptible +import java.io.IOException +import java.nio.file.Path +import kotlin.io.path.name + +enum class Unarchiver { + ZIP { + override val extension = "zip" + override fun createDecompressor(file: Path) = Decompressor.Zip(file) + }, + TAR_GZ { + override val extension = "tar.gz" + override fun createDecompressor(file: Path) = Decompressor.Tar(file) + }, + TAR_XZ { + override val extension = "tar.xz" + override fun createDecompressor(file: Path) = Decompressor.Tar(file) + }, + VSIX { + override val extension = "vsix" + override fun createDecompressor(file: Path) = Decompressor.Zip(file) + }; + + protected abstract val extension: String + protected abstract fun createDecompressor(file: Path): Decompressor + + companion object { + @Throws(IOException::class) + suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { + runInterruptible { + val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } + ?: error("Unexpected archive type: $archivePath") + val dec = unarchiver.createDecompressor(archivePath) + if (prefix != null) { + dec.removePrefixPath(prefix) + } + dec.extract(dst) + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt index 7a8e4c24..8f5d76b4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt @@ -30,6 +30,8 @@ import com.intellij.platform.ide.progress.TaskCancellation import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.util.application import kotlinx.coroutines.* +import java.awt.Component +import kotlin.coroutines.CoroutineContext inline fun runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleFactory: () -> String, cancellationFactory: () -> TaskCancellation = {TaskCancellation.cancellable()}, noinline action: suspend CoroutineScope.() -> T): T { return if (application.isDispatchThread) { @@ -40,7 +42,11 @@ inline fun runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleF } suspend inline fun withEDTContext(state: ModalityState, noinline block: suspend CoroutineScope.() -> T): T { - return withContext(Dispatchers.EDT + state.asContextElement(), block = block) + return withEDTContext(state.asContextElement(), block = block) +} + +suspend inline fun withEDTContext(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T): T { + return withContext(Dispatchers.EDT + context, block = block) } suspend inline fun withCurrentEDTModalityContext(noinline block: suspend CoroutineScope.() -> T): T { @@ -49,16 +55,17 @@ suspend inline fun withCurrentEDTModalityContext(noinline block: suspend Cor } } -fun T.letBlocking(targetAction: suspend CoroutineScope.(T) -> R): R { - return runBlocking { - targetAction(this@letBlocking) - } -} - suspend inline fun runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T { - return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction) + return runInterruptibleEDT(state.asContextElement(), targetAction = targetAction) +} +suspend inline fun runInterruptibleEDT(context: CoroutineContext, noinline targetAction: () -> T): T { + return runInterruptible(Dispatchers.EDT + context, block = targetAction) } fun CoroutineScope.launchWithEDT(state: ModalityState, block: suspend CoroutineScope.() -> Unit): Job { return launch(Dispatchers.EDT + state.asContextElement(), block = block) +} + +fun Component.asContextElement(): CoroutineContext { + return ModalityState.stateForComponent(this).asContextElement() } \ No newline at end of file From ee5a2463b9347efecb7a842e1561eddff2d7f573 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Tue, 8 Apr 2025 00:36:44 +0200 Subject: [PATCH 08/22] fully functional selection logic --- .../toolchain/ZigDebuggerToolchainService.kt | 60 +++-- .../toolchain/ZigToolchainListService.kt | 35 ++- .../project/toolchain/ZigToolchainService.kt | 14 +- .../toolchain/downloader/DirectoryState.kt | 57 +++++ .../{ui => downloader}/Downloader.kt | 138 ++++------- .../toolchain/downloader/LocalSelector.kt | 104 ++++++++ .../toolchain/downloader/ZigVersionInfo.kt | 224 +++++++++++------- .../toolchain/local/LocalZigToolchain.kt | 4 +- .../ui/ZigToolchainComboBoxHandler.kt | 39 +++ .../toolchain/ui/ZigToolchainEditor.kt | 101 ++++++-- .../toolchain/ui/ZigToolchainListEditor.kt | 66 +++++- .../project/toolchain/ui/elements.kt | 8 +- .../zigbrains/shared/Unarchiver.kt | 27 ++- .../shared/coroutine/CoroutinesUtil.kt | 5 +- 14 files changed, 618 insertions(+), 264 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/DirectoryState.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{ui => downloader}/Downloader.kt (58%) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt diff --git a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt index 7feadf32..d8a8837c 100644 --- a/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt +++ b/cidr/src/main/kotlin/com/falsepattern/zigbrains/debugger/toolchain/ZigDebuggerToolchainService.kt @@ -35,6 +35,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.platform.util.progress.reportSequentialProgress import com.intellij.ui.BrowserHyperlinkListener import com.intellij.ui.HyperlinkLabel import com.intellij.ui.components.JBPanel @@ -47,6 +48,7 @@ import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager import com.jetbrains.cidr.execution.debugger.backend.bin.UrlProvider import com.jetbrains.cidr.execution.debugger.backend.lldb.LLDBDriverConfiguration import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import java.io.IOException import java.net.URL @@ -167,7 +169,9 @@ class ZigDebuggerToolchainService { } try { - downloadAndUnArchive(baseDir, downloadableBinaries) + withContext(Dispatchers.IO) { + downloadAndUnArchive(baseDir, downloadableBinaries) + } return DownloadResult.Ok(baseDir) } catch (e: IOException) { //TODO logging @@ -206,34 +210,40 @@ class ZigDebuggerToolchainService { @Throws(IOException::class) @RequiresEdt private suspend fun downloadAndUnArchive(baseDir: Path, binariesToDownload: List) { - val service = DownloadableFileService.getInstance() + reportSequentialProgress { reporter -> + val service = DownloadableFileService.getInstance() - val downloadDir = baseDir.toFile() - downloadDir.deleteRecursively() + val downloadDir = baseDir.toFile() + downloadDir.deleteRecursively() - val descriptions = binariesToDownload.map { - service.createFileDescription(it.url, fileName(it.url)) - } - - val downloader = service.createDownloader(descriptions, "Debugger downloading") - val downloadDirectory = downloadPath().toFile() - val downloadResults = withContext(Dispatchers.IO) { - coroutineToIndicator { - downloader.download(downloadDirectory) + val descriptions = binariesToDownload.map { + service.createFileDescription(it.url, fileName(it.url)) } - } - val versions = Properties() - for (result in downloadResults) { - val downloadUrl = result.second.downloadUrl - val binaryToDownload = binariesToDownload.first { it.url == downloadUrl } - val propertyName = binaryToDownload.propertyName - val archiveFile = result.first - Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix) - archiveFile.delete() - versions[propertyName] = binaryToDownload.version - } - saveVersionsFile(baseDir, versions) + val downloader = service.createDownloader(descriptions, "Debugger downloading") + val downloadDirectory = downloadPath().toFile() + val downloadResults = reporter.sizedStep(100) { + coroutineToIndicator { + downloader.download(downloadDirectory) + } + } + val versions = Properties() + for (result in downloadResults) { + val downloadUrl = result.second.downloadUrl + val binaryToDownload = binariesToDownload.first { it.url == downloadUrl } + val propertyName = binaryToDownload.propertyName + val archiveFile = result.first + reporter.indeterminateStep { + coroutineToIndicator { + Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix) + } + } + archiveFile.delete() + versions[propertyName] = binaryToDownload.version + } + + saveVersionsFile(baseDir, versions) + } } private fun lldbUrls(): Pair? { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index 740f551a..1d38ea2d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -25,7 +25,9 @@ package com.falsepattern.zigbrains.project.toolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.resolve import com.falsepattern.zigbrains.project.toolchain.base.toRef +import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.components.* +import kotlinx.coroutines.launch import java.lang.ref.WeakReference import java.util.UUID @@ -37,19 +39,43 @@ import java.util.UUID class ZigToolchainListService: SerializablePersistentStateComponent(State()) { private val changeListeners = ArrayList>() fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { + val str = uuid.toString() + val ref = toolchain.toRef() updateState { val newMap = HashMap() newMap.putAll(it.toolchains) - newMap[uuid.toString()] = toolchain.toRef() + newMap[str] = ref it.copy(toolchains = newMap) } notifyChanged() } + fun registerNewToolchain(toolchain: ZigToolchain): UUID { + val ref = toolchain.toRef() + var uuid = UUID.randomUUID() + updateState { + val newMap = HashMap() + newMap.putAll(it.toolchains) + var uuidStr = uuid.toString() + while (newMap.containsKey(uuidStr)) { + uuid = UUID.randomUUID() + uuidStr = uuid.toString() + } + newMap[uuidStr] = ref + it.copy(toolchains = newMap) + } + notifyChanged() + return uuid + } + fun getToolchain(uuid: UUID): ZigToolchain? { return state.toolchains[uuid.toString()]?.resolve() } + fun hasToolchain(uuid: UUID): Boolean { + return state.toolchains.containsKey(uuid.toString()) + } + fun removeToolchain(uuid: UUID) { val str = uuid.toString() updateState { @@ -67,7 +93,9 @@ class ZigToolchainListService: SerializablePersistentStateComponent> get() = state.toolchains .asSequence() @@ -109,6 +138,6 @@ class ZigToolchainListService: SerializablePersistentStateComponent(State()) { var toolchainUUID: UUID? - get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) } + get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { + if (ZigToolchainListService.getInstance().hasToolchain(it)) { + true + } else { + updateState { + it.copy(toolchain = "") + } + false + } + } set(value) { updateState { it.copy(toolchain = value?.toString() ?: "") @@ -49,11 +58,10 @@ class ZigToolchainService: SerializablePersistentStateComponent false + CreateNew, Ok -> true + } + } + + companion object { + @JvmStatic + fun determine(path: Path?): DirectoryState { + if (path == null) { + return Invalid + } + if (!path.isAbsolute) { + return NotAbsolute + } + if (!path.exists()) { + var parent: Path? = path.parent + while(parent != null) { + if (!parent.exists()) { + parent = parent.parent + continue + } + if (!parent.isDirectory()) { + return NotDirectory + } + return CreateNew + } + return Invalid + } + if (!path.isDirectory()) { + return NotDirectory + } + val isEmpty = Files.newDirectoryStream(path).use { !it.iterator().hasNext() } + if (!isEmpty) { + return NotEmpty + } + return Ok + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt similarity index 58% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt index f7770eb8..5cf76ace 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt @@ -20,14 +20,16 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.ui +package com.falsepattern.zigbrains.project.toolchain.downloader import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.intellij.icons.AllIcons import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.observable.util.whenFocusGained import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.util.Disposer @@ -43,95 +45,38 @@ import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.panel import com.intellij.util.asSafely -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import com.intellij.util.concurrency.annotations.RequiresEdt import java.awt.Component -import java.nio.file.Files import java.nio.file.Path -import java.util.UUID +import java.util.* import javax.swing.DefaultComboBoxModel import javax.swing.JList import javax.swing.event.DocumentEvent -import kotlin.contracts.ExperimentalContracts -import kotlin.io.path.exists -import kotlin.io.path.isDirectory //TODO lang object Downloader { - suspend fun downloadToolchain(component: Component): UUID? { + suspend fun downloadToolchain(component: Component): ZigToolchain? { val info = withModalProgress( ModalTaskOwner.component(component), "Fetching zig version information", - TaskCancellation.cancellable()) { - withContext(Dispatchers.IO) { - ZigVersionInfo.downloadVersionList() - } + TaskCancellation.cancellable() + ) { + ZigVersionInfo.downloadVersionList() } val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) { selectToolchain(info) } ?: return null withModalProgress( ModalTaskOwner.component(component), - "Downloading zig tarball", - TaskCancellation.cancellable()) { - withContext(Dispatchers.IO) { - version.downloadAndUnpack(downloadPath) - } - } - return null - } - - private enum class DirectoryState { - Invalid, - NotAbsolute, - NotDirectory, - NotEmpty, - CreateNew, - Ok; - - fun isValid(): Boolean { - return when(this) { - Invalid, NotAbsolute, NotDirectory, NotEmpty -> false - CreateNew, Ok -> true - } - } - - companion object { - @OptIn(ExperimentalContracts::class) - @JvmStatic - fun determine(path: Path?): DirectoryState { - if (path == null) { - return Invalid - } - if (!path.isAbsolute) { - return NotAbsolute - } - if (!path.exists()) { - var parent: Path? = path.parent - while(parent != null) { - if (!parent.exists()) { - parent = parent.parent - continue - } - if (!parent.isDirectory()) { - return NotDirectory - } - return CreateNew - } - return Invalid - } - if (!path.isDirectory()) { - return NotDirectory - } - val isEmpty = Files.newDirectoryStream(path).use { !it.iterator().hasNext() } - if (!isEmpty) { - return NotEmpty - } - return Ok - } + "Installing Zig ${version.version}", + TaskCancellation.cancellable() + ) { + version.downloadAndUnpack(downloadPath) } + return LocalZigToolchain.tryFromPath(downloadPath) } + @RequiresEdt private fun selectToolchain(info: List): Pair? { val dialog = DialogBuilder() val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray())) @@ -148,32 +93,39 @@ object Downloader { } val outputPath = textFieldWithBrowseButton( null, - FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) ) Disposer.register(dialog, outputPath) outputPath.textField.columns = 50 lateinit var errorMessageBox: JBLabel + fun onChanged() { + val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() + val state = DirectoryState.determine(path) + if (state.isValid()) { + errorMessageBox.icon = AllIcons.General.Information + dialog.setOkActionEnabled(true) + } else { + errorMessageBox.icon = AllIcons.General.Error + dialog.setOkActionEnabled(false) + } + errorMessageBox.text = when(state) { + DirectoryState.Invalid -> "Invalid path" + DirectoryState.NotAbsolute -> "Must be an absolute path" + DirectoryState.NotDirectory -> "Path is not a directory" + DirectoryState.NotEmpty -> "Directory is not empty" + DirectoryState.CreateNew -> "Directory will be created" + DirectoryState.Ok -> "Directory OK" + } + dialog.window.repaint() + } + outputPath.whenFocusGained { + onChanged() + } outputPath.addDocumentListener(object: DocumentAdapter() { override fun textChanged(e: DocumentEvent) { - val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() - val state = DirectoryState.determine(path) - if (state.isValid()) { - errorMessageBox.icon = AllIcons.General.Information - dialog.setOkActionEnabled(true) - } else { - errorMessageBox.icon = AllIcons.General.Error - dialog.setOkActionEnabled(false) - } - errorMessageBox.text = when(state) { - DirectoryState.Invalid -> "Invalid path" - DirectoryState.NotAbsolute -> "Must be an absolute path" - DirectoryState.NotDirectory -> "Path is not a directory" - DirectoryState.NotEmpty -> "Directory is not empty" - DirectoryState.CreateNew -> "Directory will be created" - DirectoryState.Ok -> "Directory OK" - } - dialog.window.repaint() + onChanged() } }) var archiveSizeCell: Cell<*>? = null @@ -200,7 +152,7 @@ object Downloader { } detect(info[0]) dialog.centerPanel(center) - dialog.setTitle("Version Selector") + dialog.setTitle("Zig Downloader") dialog.addCancelAction() dialog.addOkAction().also { it.setText("Download") } if (!dialog.showAndGet()) { @@ -216,8 +168,4 @@ object Downloader { return path to version } - - private suspend fun installToolchain(path: Path, version: ZigVersionInfo): Boolean { - TODO("Not yet implemented") - } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt new file mode 100644 index 00000000..0f74a927 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt @@ -0,0 +1,104 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.downloader + +import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT +import com.intellij.icons.AllIcons +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.util.Disposer +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.annotations.RequiresEdt +import java.awt.Component +import javax.swing.event.DocumentEvent + +object LocalSelector { + suspend fun browseFromDisk(component: Component): ZigToolchain? { + return runInterruptibleEDT(component.asContextElement()) { + doBrowseFromDisk() + } + } + + @RequiresEdt + private fun doBrowseFromDisk(): ZigToolchain? { + val dialog = DialogBuilder() + val path = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + ) + Disposer.register(dialog, path) + path.textField.columns = 50 + lateinit var errorMessageBox: JBLabel + path.addDocumentListener(object: DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + val tc = LocalZigToolchain.tryFromPathString(path.text) + if (tc == null) { + errorMessageBox.icon = AllIcons.General.Error + errorMessageBox.text = "Invalid toolchain path" + dialog.setOkActionEnabled(false) + } else if (ZigToolchainListService + .getInstance() + .toolchains + .mapNotNull { it.second as? LocalZigToolchain } + .any { it.location == tc.location } + ) { + errorMessageBox.icon = AllIcons.General.Warning + errorMessageBox.text = tc.name?.let { "Toolchain already exists as \"$it\"" } ?: "Toolchain already exists" + dialog.setOkActionEnabled(true) + } else { + errorMessageBox.icon = Icons.Zig + errorMessageBox.text = tc.name ?: "OK" + dialog.setOkActionEnabled(true) + } + } + }) + val center = panel { + row("Path:") { + cell(path).resizableColumn().align(AlignX.FILL) + } + row { + errorMessageBox = JBLabel() + cell(errorMessageBox) + } + } + dialog.centerPanel(center) + dialog.setTitle("Zig Browser") + dialog.addCancelAction() + dialog.addOkAction().also { it.setText("Add") } + if (!dialog.showAndGet()) { + return null + } + return LocalZigToolchain.tryFromPathString(path.text) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt index 09e8ba2f..b2ba7f8a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt @@ -24,12 +24,12 @@ package com.falsepattern.zigbrains.project.toolchain.downloader import com.falsepattern.zigbrains.shared.Unarchiver import com.intellij.openapi.application.PathManager +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.progress.coroutineToIndicator import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.platform.util.progress.reportProgress -import com.intellij.platform.util.progress.reportSequentialProgress -import com.intellij.platform.util.progress.withProgressText +import com.intellij.platform.util.progress.* import com.intellij.util.asSafely import com.intellij.util.download.DownloadableFileService import com.intellij.util.io.createDirectories @@ -38,6 +38,8 @@ import com.intellij.util.io.move import com.intellij.util.system.CpuArch import com.intellij.util.system.OS import com.intellij.util.text.SemVer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -47,9 +49,14 @@ 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 +import kotlin.io.path.name @JvmRecord data class ZigVersionInfo( @@ -60,108 +67,145 @@ data class ZigVersionInfo( val src: Tarball?, val dist: Tarball ) { - suspend fun downloadAndUnpack(into: Path): Boolean { - return reportProgress { reporter -> - try { - into.createDirectories() - } catch (e: Exception) { - return@reportProgress false - } - val service = DownloadableFileService.getInstance() - val fileName = dist.tarball.substringAfterLast('/') - val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) - val desc = service.createFileDescription(dist.tarball, tempFile.name) - val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") - val downloadResults = reporter.sizedStep(100) { - coroutineToIndicator { - downloader.download(into.toFile()) - } - } - if (downloadResults.isEmpty()) - return@reportProgress false - val tarball = downloadResults[0].first - reporter.indeterminateStep("Extracting tarball") { - Unarchiver.unarchive(tarball.toPath(), into) - tarball.delete() - val contents = Files.newDirectoryStream(into).use { it.toList() } - if (contents.size == 1 && contents[0].isDirectory()) { - val src = contents[0] - Files.newDirectoryStream(src).use { stream -> - stream.forEach { - it.move(into.resolve(src.relativize(it))) - } - } - src.delete() - } - } - return@reportProgress true + @Throws(Exception::class) + suspend fun downloadAndUnpack(into: Path) { + reportProgress { reporter -> + into.createDirectories() + val tarball = downloadTarball(dist, into, reporter) + unpackTarball(tarball, into, reporter) + tarball.delete() + flattenDownloadDir(into, reporter) } } + companion object { @OptIn(ExperimentalSerializationApi::class) suspend fun downloadVersionList(): List { - val service = DownloadableFileService.getInstance() - val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false) - val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name) - val downloader = service.createDownloader(listOf(desc), "Zig version information downloading") - val downloadResults = coroutineToIndicator { - downloader.download(tempPluginDir) + return withContext(Dispatchers.IO) { + val service = DownloadableFileService.getInstance() + val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false) + val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name) + val downloader = service.createDownloader(listOf(desc), "Zig version information") + val downloadResults = coroutineToIndicator { + downloader.download(tempPluginDir) + } + if (downloadResults.isEmpty()) + return@withContext emptyList() + val index = downloadResults[0].first + val info = index.inputStream().use { Json.decodeFromStream(it) } + index.delete() + return@withContext info.mapNotNull { (version, data) -> parseVersion(version, data) }.toList() } - if (downloadResults.isEmpty()) - return emptyList() - val index = downloadResults[0].first - val info = index.inputStream().use { Json.decodeFromStream(it) } - index.delete() - return info.mapNotNull { (version, data) -> parseVersion(version, data) }.toList() } + } - private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? { - if (data !is JsonObject) - return null + @JvmRecord + @Serializable + data class Tarball(val tarball: String, val shasum: String, val size: Int) +} - val versionTag = data["version"]?.asSafely()?.content - - val version = SemVer.parseFromText(versionTag) ?: SemVer.parseFromText(versionKey) - ?: return null - val date = data["date"]?.asSafely()?.content ?: "" - val docs = data["docs"]?.asSafely()?.content ?: "" - val notes = data["notes"]?.asSafely()?.content?: "" - val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } - val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } - ?: return null - - - return ZigVersionInfo(version, date, docs, notes, src, dist) +private suspend fun downloadTarball(dist: ZigVersionInfo.Tarball, into: Path, reporter: ProgressReporter): Path { + return withContext(Dispatchers.IO) { + val service = DownloadableFileService.getInstance() + val fileName = dist.tarball.substringAfterLast('/') + val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) + val desc = service.createFileDescription(dist.tarball, tempFile.name) + val downloader = service.createDownloader(listOf(desc), "Zig tarball") + val downloadResults = reporter.sizedStep(100) { + coroutineToIndicator { + downloader.download(into.toFile()) + } } + if (downloadResults.isEmpty()) + throw IllegalStateException("No file downloaded") + return@withContext downloadResults[0].first.toPath() + } +} - private fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { - if (!dist.contains('-')) - return null - val (arch, os) = dist.split('-', limit = 2) - val theArch = when (arch) { - "x86_64" -> CpuArch.X86_64 - "i386" -> CpuArch.X86 - "armv7a" -> CpuArch.ARM32 - "aarch64" -> CpuArch.ARM64 - else -> return null +private suspend fun flattenDownloadDir(dir: Path, reporter: ProgressReporter) { + withContext(Dispatchers.IO) { + val contents = Files.newDirectoryStream(dir).use { it.toList() } + if (contents.size == 1 && contents[0].isDirectory()) { + val src = contents[0] + reporter.indeterminateStep { + coroutineToIndicator { + val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator() + indicator.isIndeterminate = true + indicator.text = "Flattening directory" + Files.newDirectoryStream(src).use { stream -> + stream.forEach { + indicator.text2 = it.name + it.move(dir.resolve(src.relativize(it))) + } + } + } } - val theOS = when (os) { - "linux" -> OS.Linux - "windows" -> OS.Windows - "macos" -> OS.macOS - "freebsd" -> OS.FreeBSD - else -> return null - } - if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) { - return null - } - return Json.decodeFromJsonElement(tb) + src.delete() } } } -@JvmRecord -@Serializable -data class Tarball(val tarball: String, val shasum: String, val size: Int) +@OptIn(ExperimentalPathApi::class) +private suspend fun unpackTarball(tarball: Path, into: Path, reporter: ProgressReporter) { + withContext(Dispatchers.IO) { + try { + reporter.indeterminateStep { + coroutineToIndicator { + Unarchiver.unarchive(tarball, into) + } + } + } catch (e: Throwable) { + tarball.delete() + val contents = Files.newDirectoryStream(into).use { it.toList() } + if (contents.size == 1 && contents[0].isDirectory()) { + contents[0].deleteRecursively() + } + throw e + } + } +} + +private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? { + if (data !is JsonObject) + return null + + val versionTag = data["version"]?.asSafely()?.content + + val version = SemVer.parseFromText(versionTag) ?: SemVer.parseFromText(versionKey) + ?: return null + val date = data["date"]?.asSafely()?.content ?: "" + val docs = data["docs"]?.asSafely()?.content ?: "" + val notes = data["notes"]?.asSafely()?.content ?: "" + val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } + val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } + ?: return null + + + return ZigVersionInfo(version, date, docs, notes, src, dist) +} + +private fun getTarballIfCompatible(dist: String, tb: JsonElement): ZigVersionInfo.Tarball? { + if (!dist.contains('-')) + return null + val (arch, os) = dist.split('-', limit = 2) + val theArch = when (arch) { + "x86_64" -> CpuArch.X86_64 + "i386" -> CpuArch.X86 + "armv7a" -> CpuArch.ARM32 + "aarch64" -> CpuArch.ARM64 + else -> return null + } + val theOS = when (os) { + "linux" -> OS.Linux + "windows" -> OS.Windows + "macos" -> OS.macOS + "freebsd" -> OS.FreeBSD + else -> return null + } + if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) { + return null + } + return Json.decodeFromJsonElement(tb) +} private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index f23cb30b..6062afc6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -65,8 +65,8 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override } } - fun tryFromPathString(pathStr: String): LocalZigToolchain? { - return pathStr.toNioPathOrNull()?.let(::tryFromPath) + fun tryFromPathString(pathStr: String?): LocalZigToolchain? { + return pathStr?.ifBlank { null }?.toNioPathOrNull()?.let(::tryFromPath) } fun tryFromPath(path: Path): LocalZigToolchain? { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt new file mode 100644 index 00000000..9354cf5a --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt @@ -0,0 +1,39 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader +import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread +import java.awt.Component +import java.util.UUID + +internal object ZigToolchainComboBoxHandler { + @RequiresBackgroundThread + suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) { + is TCListElem.Toolchain.Suggested -> elem.toolchain + is TCListElem.Download -> Downloader.downloadToolchain(context) + is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context) + }?.let { ZigToolchainListService.getInstance().registerNewToolchain(it) } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt index e83c00e1..f7749990 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 @@ -25,15 +25,33 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +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.ApplicationManager +import com.intellij.openapi.application.EDT import com.intellij.openapi.options.Configurable +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.options.newEditor.SettingsDialog +import com.intellij.openapi.options.newEditor.SettingsTreeView import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.NlsContexts import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.annotations.RequiresEdt +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import java.awt.event.ItemEvent +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.util.UUID import javax.swing.JComponent import kotlin.collections.addAll @@ -74,7 +92,7 @@ class ZigToolchainEditor(private val project: Project): Configurable { inner class UI(): Disposable, ZigToolchainListService.ToolchainListChangeListener { private val toolchainBox: TCComboBox - private var oldSelectionIndex: Int = 0 + private var selectOnNextReload: UUID? = null private val model: TCModel init { model = TCModel(getModelList()) @@ -89,27 +107,44 @@ class ZigToolchainEditor(private val project: Project): Configurable { return } val item = event.item - if (item !is TCListElem) { - toolchainBox.selectedIndex = oldSelectionIndex + if (item !is TCListElem.Pseudo) return - } - when(item) { - is TCListElem.None, is TCListElem.Toolchain.Actual -> { - oldSelectionIndex = toolchainBox.selectedIndex - } - else -> { - toolchainBox.selectedIndex = oldSelectionIndex + zigCoroutineScope.launch(toolchainBox.asContextElement()) { + val uuid = ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) + withEDTContext(toolchainBox.asContextElement()) { + applyUUIDNowOrOnReload(uuid) } } } - override fun toolchainListChanged() { - val selected = model.selected - val list = getModelList() - model.updateContents(list) - if (selected != null && list.contains(selected)) { - model.selectedItem = selected - } else { + override suspend fun toolchainListChanged() { + withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { + val list = getModelList() + 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 } } @@ -117,6 +152,35 @@ class ZigToolchainEditor(private val project: Project): Configurable { fun attach(p: Panel): Unit = with(p) { row("Toolchain") { cell(toolchainBox).resizableColumn().align(AlignX.FILL) + button("Funny") { e -> + zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) { + val config = ZigToolchainListEditor() + var inited = false + var selectedUUID: UUID? = toolchainBox.selectedToolchain + config.addItemSelectedListener { + if (inited) { + selectedUUID = it + } + } + val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config) { + config.selectNodeInTree(selectedUUID) + inited = true + } + if (apply) { + applyUUIDNowOrOnReload(selectedUUID) + } + } + } + } + } + + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + toolchainBox.selectedToolchain = uuid + if (uuid != null && toolchainBox.selectedToolchain == null) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null } } @@ -130,9 +194,10 @@ class ZigToolchainEditor(private val project: Project): Configurable { fun reset() { toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID - oldSelectionIndex = toolchainBox.selectedIndex } + + override fun dispose() { ZigToolchainListService.getInstance().removeChangeListener(this) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index 0d202210..84194464 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -22,27 +22,56 @@ package com.falsepattern.zigbrains.project.toolchain.ui +import com.falsepattern.zigbrains.Icons 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.downloader.Downloader +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.application.EDT +import com.intellij.openapi.components.Service +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.MasterDetailsComponent +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.Consumer import com.intellij.util.IconUtil +import com.intellij.util.asSafely +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.UUID import javax.swing.JComponent +import javax.swing.event.DocumentEvent import javax.swing.tree.DefaultTreeModel -class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener { +class ZigToolchainListEditor : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener { private var isTreeInitialized = false private var registered: Boolean = false + private var itemSelectedListeners = ArrayList>() + + fun addItemSelectedListener(c: Consumer) { + synchronized(itemSelectedListeners) { + itemSelectedListeners.add(c) + } + } override fun createComponent(): JComponent { if (!isTreeInitialized) { @@ -72,6 +101,14 @@ class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListServi return listOf(add, MyDeleteAction()) } + override fun updateSelection(configurable: NamedConfigurable<*>?) { + super.updateSelection(configurable) + val uuid = configurable?.editableObject as? UUID + synchronized(itemSelectedListeners) { + itemSelectedListeners.forEach { it.consume(uuid) } + } + } + override fun onItemDeleted(item: Any?) { if (item is UUID) { ZigToolchainListService.getInstance().removeToolchain(item) @@ -80,18 +117,15 @@ class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListServi } private fun onItemSelected(elem: TCListElem) { - when (elem) { - is TCListElem.Toolchain -> { - val uuid = UUID.randomUUID() - ZigToolchainListService.getInstance().setToolchain(uuid, elem.toolchain) - } - is TCListElem.Download -> { - zigCoroutineScope.launch(myWholePanel.asContextElement()) { - Downloader.downloadToolchain(myWholePanel) + if (elem !is TCListElem.Pseudo) + return + zigCoroutineScope.launch(myWholePanel.asContextElement()) { + val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem) + if (uuid != null) { + withEDTContext(myWholePanel.asContextElement()) { + selectNodeInTree(uuid) } } - is TCListElem.FromDisk -> {} - is TCListElem.None -> {} } } @@ -110,11 +144,15 @@ class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListServi } private fun reloadTree() { + val currentSelection = selectedObject?.asSafely() myRoot.removeAllChildren() ZigToolchainListService.getInstance().toolchains.forEach { (uuid, toolchain) -> addToolchain(uuid, toolchain) } (myTree.model as DefaultTreeModel).reload() + currentSelection?.let { + selectNodeInTree(it) + } } override fun disposeUIResources() { @@ -124,7 +162,9 @@ class ZigToolchainListEditor() : MasterDetailsComponent(), ZigToolchainListServi } } - override fun toolchainListChanged() { - reloadTree() + override suspend fun toolchainListChanged() { + withEDTContext(myWholePanel.asContextElement()) { + reloadTree() + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt index 45045252..1915f91b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt @@ -27,20 +27,20 @@ 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 + 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 - object FromDisk : TCListElem + object Download : TCListElem, Pseudo + object FromDisk : TCListElem, Pseudo companion object { val fetchGroup get() = listOf(Download, FromDisk) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt index 578567f1..acc89145 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/Unarchiver.kt @@ -22,8 +22,9 @@ package com.falsepattern.zigbrains.shared +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.progress.ProgressManager import com.intellij.util.io.Decompressor -import kotlinx.coroutines.runInterruptible import java.io.IOException import java.nio.file.Path import kotlin.io.path.name @@ -51,16 +52,22 @@ enum class Unarchiver { companion object { @Throws(IOException::class) - suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { - runInterruptible { - val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } - ?: error("Unexpected archive type: $archivePath") - val dec = unarchiver.createDecompressor(archivePath) - if (prefix != null) { - dec.removePrefixPath(prefix) - } - dec.extract(dst) + fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) { + val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } + ?: error("Unexpected archive type: $archivePath") + val dec = unarchiver.createDecompressor(archivePath) + val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator() + indicator.isIndeterminate = true + indicator.text = "Extracting archive" + dec.filter { + indicator.text2 = it + indicator.checkCanceled() + true } + if (prefix != null) { + dec.removePrefixPath(prefix) + } + dec.extract(dst) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt index 8f5d76b4..2b61b2e8 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/coroutine/CoroutinesUtil.kt @@ -63,7 +63,10 @@ suspend inline fun runInterruptibleEDT(context: CoroutineContext, noinline t } fun CoroutineScope.launchWithEDT(state: ModalityState, block: suspend CoroutineScope.() -> Unit): Job { - return launch(Dispatchers.EDT + state.asContextElement(), block = block) + return launchWithEDT(state.asContextElement(), block = block) +} +fun CoroutineScope.launchWithEDT(context: CoroutineContext, block: suspend CoroutineScope.() -> Unit): Job { + return launch(Dispatchers.EDT + context, block = block) } fun Component.asContextElement(): CoroutineContext { From b485c1e48c3eb6f4ba52b1e5d2e84f900efc2786 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Wed, 9 Apr 2025 00:48:08 +0200 Subject: [PATCH 09/22] lang for toolchain --- .../toolchain/base/ZigToolchainPanel.kt | 3 +- .../toolchain/downloader/Downloader.kt | 33 ++++++------- .../toolchain/downloader/LocalSelector.kt | 15 +++--- .../toolchain/downloader/ZigVersionInfo.kt | 7 +-- .../toolchain/local/LocalZigToolchainPanel.kt | 6 +-- .../toolchain/ui/ZigToolchainEditor.kt | 9 ++-- .../toolchain/ui/ZigToolchainListEditor.kt | 8 +-- .../zigbrains/project/toolchain/ui/model.kt | 7 +-- .../project/toolchain/ui/popup/popup.kt | 49 ------------------- .../resources/zigbrains/Bundle.properties | 41 +++++++++++++++- 10 files changed, 85 insertions(+), 93 deletions(-) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt index ab747375..7908af87 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.ZigBrainsBundle import com.intellij.openapi.Disposable import com.intellij.ui.components.JBTextField import com.intellij.ui.dsl.builder.AlignX @@ -35,7 +36,7 @@ abstract class ZigToolchainPanel: Disposable { set(value) {nameField.text = value ?: ""} open fun attach(p: Panel): Unit = with(p) { - row("Name") { + row(ZigBrainsBundle.message("settings.toolchain.base.name.label")) { cell(nameField).resizableColumn().align(AlignX.FILL) } separator() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt index 5cf76ace..e72aea09 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt @@ -53,12 +53,11 @@ import javax.swing.DefaultComboBoxModel import javax.swing.JList import javax.swing.event.DocumentEvent -//TODO lang object Downloader { suspend fun downloadToolchain(component: Component): ZigToolchain? { val info = withModalProgress( ModalTaskOwner.component(component), - "Fetching zig version information", + ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch"), TaskCancellation.cancellable() ) { ZigVersionInfo.downloadVersionList() @@ -68,7 +67,7 @@ object Downloader { } ?: return null withModalProgress( ModalTaskOwner.component(component), - "Installing Zig ${version.version}", + ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion), TaskCancellation.cancellable() ) { version.downloadAndUnpack(downloadPath) @@ -94,7 +93,7 @@ object Downloader { val outputPath = textFieldWithBrowseButton( null, FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + .withTitle(ZigBrainsBundle.message("settings.toolchain.downloader.chooser.title")) ) Disposer.register(dialog, outputPath) outputPath.textField.columns = 50 @@ -110,14 +109,14 @@ object Downloader { errorMessageBox.icon = AllIcons.General.Error dialog.setOkActionEnabled(false) } - errorMessageBox.text = when(state) { - DirectoryState.Invalid -> "Invalid path" - DirectoryState.NotAbsolute -> "Must be an absolute path" - DirectoryState.NotDirectory -> "Path is not a directory" - DirectoryState.NotEmpty -> "Directory is not empty" - DirectoryState.CreateNew -> "Directory will be created" - DirectoryState.Ok -> "Directory OK" - } + errorMessageBox.text = ZigBrainsBundle.message(when(state) { + DirectoryState.Invalid -> "settings.toolchain.downloader.state.invalid" + DirectoryState.NotAbsolute -> "settings.toolchain.downloader.state.not-absolute" + DirectoryState.NotDirectory -> "settings.toolchain.downloader.state.not-directory" + DirectoryState.NotEmpty -> "settings.toolchain.downloader.state.not-empty" + DirectoryState.CreateNew -> "settings.toolchain.downloader.state.create-new" + DirectoryState.Ok -> "settings.toolchain.downloader.state.ok" + }) dialog.window.repaint() } outputPath.whenFocusGained { @@ -133,16 +132,16 @@ object Downloader { outputPath.text = System.getProperty("user.home") + "/.zig/" + item.version val size = item.dist.size val sizeMb = size / (1024f * 1024f) - archiveSizeCell?.comment?.text = "Archive size: %.2fMB".format(sizeMb) + archiveSizeCell?.comment?.text = ZigBrainsBundle.message("settings.toolchain.downloader.archive-size.text", "%.2fMB".format(sizeMb)) } theList.addItemListener { detect(it.item as ZigVersionInfo) } val center = panel { - row("Version:") { + row(ZigBrainsBundle.message("settings.toolchain.downloader.version.label")) { cell(theList).resizableColumn().align(AlignX.FILL) } - row("Location:") { + row(ZigBrainsBundle.message("settings.toolchain.downloader.location.label")) { cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } } row { @@ -152,9 +151,9 @@ object Downloader { } detect(info[0]) dialog.centerPanel(center) - dialog.setTitle("Zig Downloader") + dialog.setTitle(ZigBrainsBundle.message("settings.toolchain.downloader.title")) dialog.addCancelAction() - dialog.addOkAction().also { it.setText("Download") } + dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.toolchain.downloader.ok-action")) } if (!dialog.showAndGet()) { return null } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt index 0f74a927..900006da 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt @@ -55,7 +55,7 @@ object LocalSelector { val path = textFieldWithBrowseButton( null, FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) + .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")) ) Disposer.register(dialog, path) path.textField.columns = 50 @@ -65,7 +65,7 @@ object LocalSelector { val tc = LocalZigToolchain.tryFromPathString(path.text) if (tc == null) { errorMessageBox.icon = AllIcons.General.Error - errorMessageBox.text = "Invalid toolchain path" + errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") dialog.setOkActionEnabled(false) } else if (ZigToolchainListService .getInstance() @@ -74,17 +74,18 @@ object LocalSelector { .any { it.location == tc.location } ) { errorMessageBox.icon = AllIcons.General.Warning - errorMessageBox.text = tc.name?.let { "Toolchain already exists as \"$it\"" } ?: "Toolchain already exists" + errorMessageBox.text = tc.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } + ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") dialog.setOkActionEnabled(true) } else { errorMessageBox.icon = Icons.Zig - errorMessageBox.text = tc.name ?: "OK" + errorMessageBox.text = tc.name ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") dialog.setOkActionEnabled(true) } } }) val center = panel { - row("Path:") { + row(ZigBrainsBundle.message("settings.toolchain.local-selector.path.label")) { cell(path).resizableColumn().align(AlignX.FILL) } row { @@ -93,9 +94,9 @@ object LocalSelector { } } dialog.centerPanel(center) - dialog.setTitle("Zig Browser") + dialog.setTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.title")) dialog.addCancelAction() - dialog.addOkAction().also { it.setText("Add") } + dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.toolchain.local-selector.ok-action")) } if (!dialog.showAndGet()) { return null } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt index b2ba7f8a..87e80f67 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.downloader +import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.shared.Unarchiver import com.intellij.openapi.application.PathManager import com.intellij.openapi.progress.EmptyProgressIndicator @@ -85,7 +86,7 @@ data class ZigVersionInfo( val service = DownloadableFileService.getInstance() val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false) val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name) - val downloader = service.createDownloader(listOf(desc), "Zig version information") + val downloader = service.createDownloader(listOf(desc), ZigBrainsBundle.message("settings.toolchain.downloader.service.index")) val downloadResults = coroutineToIndicator { downloader.download(tempPluginDir) } @@ -110,7 +111,7 @@ private suspend fun downloadTarball(dist: ZigVersionInfo.Tarball, into: Path, re val fileName = dist.tarball.substringAfterLast('/') val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) val desc = service.createFileDescription(dist.tarball, tempFile.name) - val downloader = service.createDownloader(listOf(desc), "Zig tarball") + val downloader = service.createDownloader(listOf(desc), ZigBrainsBundle.message("settings.toolchain.downloader.service.tarball")) val downloadResults = reporter.sizedStep(100) { coroutineToIndicator { downloader.download(into.toFile()) @@ -131,7 +132,7 @@ private suspend fun flattenDownloadDir(dir: Path, reporter: ProgressReporter) { coroutineToIndicator { val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator() indicator.isIndeterminate = true - indicator.text = "Flattening directory" + indicator.text = ZigBrainsBundle.message("settings.toolchain.downloader.progress.flatten") Files.newDirectoryStream(src).use { stream -> stream.forEach { indicator.text2 = it.name 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 d2301d35..d47568b6 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 @@ -77,13 +77,13 @@ class LocalZigToolchainPanel() : ZigToolchainPanel() { override fun attach(p: Panel): Unit = with(p) { super.attach(p) - row(ZigBrainsBundle.message("settings.project.label.toolchain")) { + row(ZigBrainsBundle.message("settings.toolchain.local.path.label")) { cell(pathToToolchain).resizableColumn().align(AlignX.FILL) } - row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) { + row(ZigBrainsBundle.message("settings.toolchain.local.version.label")) { cell(toolchainVersion) } - row(ZigBrainsBundle.message("settings.project.label.std-location")) { + row(ZigBrainsBundle.message("settings.toolchain.local.std.label")) { cell(pathToStd).resizableColumn().align(AlignX.FILL) cell(stdFieldOverride) } 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 f7749990..43ef0dc9 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 @@ -22,6 +22,7 @@ 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.ZigToolchainService import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains @@ -58,7 +59,7 @@ import kotlin.collections.addAll class ZigToolchainEditor(private val project: Project): Configurable { private var myUi: UI? = null override fun getDisplayName(): @NlsContexts.ConfigurableName String? { - return "Zig" + return ZigBrainsBundle.message("settings.toolchain.editor.display-name") } override fun createComponent(): JComponent? { @@ -150,9 +151,9 @@ class ZigToolchainEditor(private val project: Project): Configurable { } fun attach(p: Panel): Unit = with(p) { - row("Toolchain") { + row(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.label")) { cell(toolchainBox).resizableColumn().align(AlignX.FILL) - button("Funny") { e -> + button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e -> zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) { val config = ZigToolchainListEditor() var inited = false @@ -211,7 +212,7 @@ private fun getModelList(): List { modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }) modelList.add(Separator("", true)) modelList.addAll(TCListElem.fetchGroup) - modelList.add(Separator("Detected toolchains", true)) + modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) return modelList } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index 84194464..766843d5 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -86,11 +86,11 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ZigToolchainListService } override fun createActions(fromPopup: Boolean): List { - val add = object : DumbAwareAction({ "lmaoo" }, Presentation.NULL_STRING, IconUtil.addIcon) { + val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.toolchain.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) { override fun actionPerformed(e: AnActionEvent) { val modelList = ArrayList() modelList.addAll(TCListElem.fetchGroup) - modelList.add(Separator("Detected toolchains", true)) + modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) val model = TCModel.Companion(modelList) val context = TCContext(null, model) @@ -134,9 +134,9 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ZigToolchainListService super.reset() } - override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchains.empty") + override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchain.list.empty") - override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchains.title") + override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title") private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { val node = MyNode(toolchain.createNamedConfigurable(uuid)) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index e3a56704..a77b1195 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.project.toolchain.ui import ai.grazie.utils.attributes.value import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.toolchain.base.render import com.intellij.icons.AllIcons import com.intellij.openapi.project.Project @@ -208,17 +209,17 @@ internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRend is TCListElem.Download -> { icon = AllIcons.Actions.Download - append("Download Zig\u2026") + append(ZigBrainsBundle.message("settings.toolchain.model.download.text")) } is TCListElem.FromDisk -> { icon = AllIcons.General.OpenDisk - append("Add Zig from disk\u2026") + append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text")) } is TCListElem.None, null -> { icon = AllIcons.General.BalloonError - append("", SimpleTextAttributes.ERROR_ATTRIBUTES) + append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) } } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt deleted file mode 100644 index e5742ba5..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/popup/popup.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain.ui.popup - -import com.falsepattern.zigbrains.Icons -import com.falsepattern.zigbrains.project.toolchain.base.render -import com.falsepattern.zigbrains.project.toolchain.ui.Separator -import com.falsepattern.zigbrains.project.toolchain.ui.TCListElem -import com.falsepattern.zigbrains.project.toolchain.ui.TCListElemIn -import com.intellij.icons.AllIcons -import com.intellij.openapi.project.Project -import com.intellij.ui.CellRendererPanel -import com.intellij.ui.CollectionListModel -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.Consumer -import com.intellij.util.ui.EmptyIcon -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import java.awt.BorderLayout -import java.awt.Component -import java.util.IdentityHashMap -import javax.accessibility.AccessibleContext -import javax.swing.JList -import javax.swing.border.Border - diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index c9bbaea4..7c761c44 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -98,8 +98,6 @@ settings.project.label.toolchain=Toolchain location settings.project.label.toolchain-version=Detected zig version settings.project.label.override-std=Override standard library settings.project.label.std-location=Standard library location -settings.toolchains.empty=Select a toolchain to view or edit its details here -settings.toolchains.title=Toolchains toolwindow.stripe.zigbrains.build=Zig build.tool.window.tree.steps.label=Steps build.tool.window.tree.build.label=Active builds @@ -112,3 +110,42 @@ 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.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.edit-button.name=Edit +settings.toolchain.model.detected.separator=Detected toolchains +settings.toolchain.model.none.text= +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: +settings.toolchain.downloader.ok-action=Download +settings.toolchain.downloader.progress.fetch=Fetching zig version information +settings.toolchain.downloader.progress.install=Installing Zig {0} +settings.toolchain.downloader.progress.flatten=Flattening unpacked archive +settings.toolchain.downloader.chooser.title=Zig Install Directory +settings.toolchain.downloader.state.invalid=Invalid path +settings.toolchain.downloader.state.not-absolute=Must be an absolute path +settings.toolchain.downloader.state.not-directory=Path is not a directory +settings.toolchain.downloader.state.not-empty=Directory is not empty +settings.toolchain.downloader.state.create-new=Directory will be created +settings.toolchain.downloader.state.ok=Directory OK +settings.toolchain.downloader.archive-size.text=Archive size: {0} +settings.toolchain.downloader.service.index=Zig version information +settings.toolchain.downloader.service.tarball=Zig archive +settings.toolchain.local-selector.title=Select Zig From Disk +settings.toolchain.local-selector.path.label=Path: +settings.toolchain.local-selector.ok-action=Add +settings.toolchain.local-selector.chooser.title=Existing Zig Install Directory +settings.toolchain.local-selector.state.invalid=Invalid toolchain path +settings.toolchain.local-selector.state.already-exists-unnamed=Toolchain already exists +settings.toolchain.local-selector.state.already-exists-named=Toolchain already exists as "{0}" +settings.toolchain.local-selector.state.ok=OK \ No newline at end of file From a8f97172d62de68927ac967bf6b8d191dc1ec468 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Wed, 9 Apr 2025 01:08:43 +0200 Subject: [PATCH 10/22] ported core to new toolchain api direnv and zls regressed for now --- build.gradle.kts | 6 +- .../com/falsepattern/zigbrains/ZBStartup.kt | 14 - .../project/execution/base/Configuration.kt | 3 - .../project/execution/base/ZigExecConfig.kt | 9 +- .../project/execution/base/ZigProfileState.kt | 4 +- .../project/module/ZigModuleBuilder.kt | 6 +- .../project/newproject/ZigNewProjectPanel.kt | 9 +- .../newproject/ZigProjectConfigurationData.kt | 9 +- .../newproject/ZigProjectGeneratorPeer.kt | 9 +- .../zigbrains/project/run/ZigProgramRunner.kt | 4 +- .../project/settings/ZigConfigurable.kt | 32 --- .../ZigCoreProjectConfigurationProvider.kt | 11 +- .../settings/ZigProjectConfigurable.kt | 60 ----- .../ZigProjectConfigurationProvider.kt | 28 +- .../project/settings/ZigProjectSettings.kt | 55 ---- .../settings/ZigProjectSettingsPanel.kt | 211 ---------------- .../settings/ZigProjectSettingsService.kt | 60 ----- .../project/stdlib/ZigSyntheticLibrary.kt | 62 ++--- .../discovery/ZigStepDiscoveryService.kt | 4 +- .../toolchain/ZigToolchainListService.kt | 75 +++--- .../project/toolchain/ZigToolchainService.kt | 13 +- .../project/toolchain/base/ZigToolchain.kt | 16 +- .../base/ZigToolchainConfigurable.kt | 5 +- .../toolchain/base/ZigToolchainPanel.kt | 4 +- .../toolchain/downloader/Downloader.kt | 4 +- .../toolchain/local/LocalZigToolchain.kt | 11 +- .../toolchain/local/LocalZigToolchainPanel.kt | 4 +- .../local/LocalZigToolchainProvider.kt | 48 +++- .../toolchain/ui/ZigToolchainEditor.kt | 239 ++++++++---------- .../zigbrains/shared/MultiConfigurable.kt | 57 ----- .../zigbrains/shared/SubConfigurable.kt | 58 ++++- .../resources/META-INF/zigbrains-core.xml | 2 +- .../resources/zigbrains/Bundle.properties | 3 +- .../lsp/ZLSProjectConfigurationProvider.kt | 2 +- src/main/resources/META-INF/plugin.xml | 2 +- 35 files changed, 350 insertions(+), 789 deletions(-) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurable.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/MultiConfigurable.kt diff --git a/build.gradle.kts b/build.gradle.kts index 9f120e29..04c2476b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ allprojects { } } filter { - includeModule("com.redhat.devtools.intellij", "lsp4ij") +// includeModule("com.redhat.devtools.intellij", "lsp4ij") } } mavenCentral() @@ -104,12 +104,12 @@ dependencies { pluginVerifier() zipSigner() - plugin(lsp4ijPluginString) +// plugin(lsp4ijPluginString) } runtimeOnly(project(":core")) runtimeOnly(project(":cidr")) - runtimeOnly(project(":lsp")) +// runtimeOnly(project(":lsp")) } intellijPlatform { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt index 5df8c86c..5d6a1d7f 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt @@ -23,7 +23,6 @@ package com.falsepattern.zigbrains import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain import com.intellij.ide.BrowserUtil @@ -73,19 +72,6 @@ class ZBStartup: ProjectActivity { notif.notify(null) } } - //Autodetection - val zigProjectState = project.zigProjectSettings.state - if (zigProjectState.toolchainPath.isNullOrBlank()) { - val data = UserDataHolderBase() - data.putUserData(LocalZigToolchain.DIRENV_KEY, - DirenvCmd.direnvInstalled() && !project.isDefault && zigProjectState.direnv - ) - val tc = project.suggestZigToolchain(data) ?: return - if (tc is LocalZigToolchain) { - zigProjectState.toolchainPath = tc.location.pathString - project.zigProjectSettings.state = zigProjectState - } - } } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/Configuration.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/Configuration.kt index e89886a0..091ce830 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/Configuration.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/Configuration.kt @@ -24,15 +24,12 @@ package com.falsepattern.zigbrains.project.execution.base import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.execution.base.ZigConfigurable.ZigConfigModule -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.shared.cli.translateCommandline import com.falsepattern.zigbrains.shared.element.* import com.intellij.openapi.Disposable import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.options.SettingsEditor -import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.ui.TextBrowseFolderListener import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.ui.components.JBCheckBox diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt index ee148595..f9269bc9 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt @@ -22,9 +22,7 @@ package com.falsepattern.zigbrains.project.execution.base -import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.intellij.execution.ExecutionException import com.intellij.execution.Executor import com.intellij.execution.configurations.ConfigurationFactory @@ -65,9 +63,10 @@ abstract class ZigExecConfig>(project: Project, factory: Con suspend fun patchCommandLine(commandLine: GeneralCommandLine): GeneralCommandLine { - if (project.zigProjectSettings.state.direnv) { - commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) - } +// TODO direnv +// if (project.zigProjectSettings.state.direnv) { +// commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) +// } return commandLine } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt index c6779014..4e13a161 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt @@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.project.execution.base import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.execution.ZigConsoleBuilder -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking @@ -55,7 +55,7 @@ abstract class ZigProfileState> ( @Throws(ExecutionException::class) suspend fun startProcessSuspend(): ProcessHandler { - val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain")) + val toolchain = ZigToolchainService.getInstance(environment.project).toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain")) return getCommandLine(toolchain, false).startIPCAwareProcess(environment.project, emulateTerminal = true) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/module/ZigModuleBuilder.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/module/ZigModuleBuilder.kt index cb3ea5ab..f26d72bd 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/module/ZigModuleBuilder.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/module/ZigModuleBuilder.kt @@ -50,7 +50,7 @@ class ZigModuleBuilder: ModuleBuilder() { override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?): ModuleWizardStep? { val step = ZigModuleWizardStep(parentDisposable) - parentDisposable?.let { Disposer.register(it, step.peer) } + parentDisposable?.let { Disposer.register(it) { step.peer.dispose() } } return step } @@ -65,14 +65,14 @@ class ZigModuleBuilder: ModuleBuilder() { } inner class ZigModuleWizardStep(parent: Disposable?): ModuleWizardStep() { - internal val peer = ZigProjectGeneratorPeer(true).also { Disposer.register(parent ?: return@also, it) } + internal val peer = ZigProjectGeneratorPeer(true).also { Disposer.register(parent ?: return@also) {it.dispose()} } override fun getComponent(): JComponent { return peer.myComponent.withBorder() } override fun disposeUIResources() { - Disposer.dispose(peer) + Disposer.dispose(peer.newProjectPanel) } override fun updateDataModel() { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt index ac1abd24..96eee5e2 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt @@ -39,9 +39,9 @@ import com.intellij.util.ui.JBUI import javax.swing.JList import javax.swing.ListSelectionModel -class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProjectConfigurationProvider.SettingsPanelHolder { +class ZigNewProjectPanel(private var handleGit: Boolean): Disposable { private val git = JBCheckBox() - override val panels = ZigProjectConfigurationProvider.createNewProjectSettingsPanels(this).onEach { Disposer.register(this, it) } + val panels = ZigProjectConfigurationProvider.createNewProjectSettingsPanels().onEach { Disposer.register(this, it) } private val templateList = JBList(JBList.createDefaultListModel(defaultTemplates)).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION selectedIndex = 0 @@ -64,7 +64,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject fun getData(): ZigProjectConfigurationData { val selectedTemplate = templateList.selectedValue - return ZigProjectConfigurationData(handleGit && git.isSelected, panels.map { it.data }, selectedTemplate) + return ZigProjectConfigurationData(handleGit && git.isSelected, panels, selectedTemplate) } fun attach(p: Panel): Unit = with(p) { @@ -73,6 +73,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject cell(git) } } + panels.filter { it.newProjectBeforeInitSelector }.forEach { it.attach(p) } group("Zig Project Template") { row { resizableRow() @@ -81,7 +82,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject .align(AlignY.FILL) } } - panels.forEach { it.attach(p) } + panels.filter { !it.newProjectBeforeInitSelector }.forEach { it.attach(p) } } override fun dispose() { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectConfigurationData.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectConfigurationData.kt index ab425caf..1658b801 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectConfigurationData.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectConfigurationData.kt @@ -22,9 +22,10 @@ package com.falsepattern.zigbrains.project.newproject -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.template.ZigInitTemplate import com.falsepattern.zigbrains.project.template.ZigProjectTemplate +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -42,7 +43,7 @@ import kotlinx.coroutines.launch @JvmRecord data class ZigProjectConfigurationData( val git: Boolean, - val conf: List, + val conf: List>, val selectedTemplate: ZigProjectTemplate ) { @RequiresBackgroundThread @@ -54,9 +55,7 @@ data class ZigProjectConfigurationData( if (!reporter.indeterminateStep("Initializing project") { if (template is ZigInitTemplate) { - val toolchain = conf - .mapNotNull { it as? ZigProjectConfigurationProvider.ToolchainProvider } - .firstNotNullOfOrNull { it.toolchain } ?: run { + val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run { Notification( "zigbrains", "Tried to generate project with zig init, but zig toolchain is invalid", diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectGeneratorPeer.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectGeneratorPeer.kt index 7d5bfa06..b80be707 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectGeneratorPeer.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigProjectGeneratorPeer.kt @@ -31,9 +31,9 @@ import com.intellij.platform.ProjectGeneratorPeer import com.intellij.ui.dsl.builder.panel import javax.swing.JComponent -class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer, Disposable { - private val newProjectPanel by lazy { - ZigNewProjectPanel(handleGit).also { Disposer.register(this, it) } +class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer { + val newProjectPanel by lazy { + ZigNewProjectPanel(handleGit) } val myComponent: JComponent by lazy { panel { @@ -61,6 +61,7 @@ class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer>(protected val val state = castProfileState(baseState) ?: return null - val toolchain = environment.project.zigProjectSettings.state.toolchain ?: run { + val toolchain = ZigToolchainService.getInstance(environment.project).toolchain ?: run { Notification( "zigbrains", "Zig project toolchain not set, cannot execute program! Please configure it in [Settings | Languages & Frameworks | Zig]", diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt deleted file mode 100644 index e16b058f..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.settings - -import com.falsepattern.zigbrains.shared.MultiConfigurable -import com.intellij.openapi.project.Project - -class ZigConfigurable(project: Project): MultiConfigurable(ZigProjectConfigurationProvider.createConfigurables(project)) { - override fun getDisplayName(): String { - return "Zig" - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt index 207da863..47a30bec 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt @@ -22,20 +22,21 @@ package com.falsepattern.zigbrains.project.settings +import com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainEditor import com.falsepattern.zigbrains.shared.SubConfigurable +import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager class ZigCoreProjectConfigurationProvider: ZigProjectConfigurationProvider { override fun handleMainConfigChanged(project: Project) { } - override fun createConfigurable(project: Project): SubConfigurable { - return ZigProjectConfigurable(project) + override fun createConfigurable(project: Project): Configurable { + return ZigToolchainEditor.Adapter(project) } - override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel { - return ZigProjectSettingsPanel(holder, ProjectManager.getInstance().defaultProject) + override fun createNewProjectSettingsPanel(): SubConfigurable { + return ZigToolchainEditor().also { it.reset(null) } } override val priority: Int diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurable.kt deleted file mode 100644 index ff333f79..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurable.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.settings - -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 ZigProjectConfigurable(private val project: Project): SubConfigurable { - private var settingsPanel: ZigProjectSettingsPanel? = null - override fun createComponent(holder: ZigProjectConfigurationProvider.SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel { - settingsPanel?.let { Disposer.dispose(it) } - val sp = ZigProjectSettingsPanel(holder, project).apply { attach(panel) }.also { Disposer.register(this, it) } - settingsPanel = sp - return sp - } - - override fun isModified(): Boolean { - return project.zigProjectSettings.isModified(settingsPanel?.data ?: return false) - } - - override fun apply() { - val service = project.zigProjectSettings - val data = settingsPanel?.data ?: return - val modified = service.isModified(data) - service.state = data - if (modified) { - ZigProjectConfigurationProvider.mainConfigChanged(project) - } - } - - override fun reset() { - settingsPanel?.data = project.zigProjectSettings.state - } - - override fun dispose() { - settingsPanel = null - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt index af4c481d..9932c3a7 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt @@ -22,42 +22,26 @@ package com.falsepattern.zigbrains.project.settings -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.Disposable import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project -import com.intellij.ui.dsl.builder.Panel interface ZigProjectConfigurationProvider { fun handleMainConfigChanged(project: Project) - fun createConfigurable(project: Project): SubConfigurable - fun createNewProjectSettingsPanel(holder: SettingsPanelHolder): SettingsPanel? + fun createConfigurable(project: Project): Configurable + fun createNewProjectSettingsPanel(): SubConfigurable? val priority: Int companion object { private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.projectConfigProvider") fun mainConfigChanged(project: Project) { EXTENSION_POINT_NAME.extensionList.forEach { it.handleMainConfigChanged(project) } } - fun createConfigurables(project: Project): List { + fun createConfigurables(project: Project): List { return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.map { it.createConfigurable(project) } } - fun createNewProjectSettingsPanels(holder: SettingsPanelHolder): List { - return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.mapNotNull { it.createNewProjectSettingsPanel(holder) } + fun createNewProjectSettingsPanels(): List> { + return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.mapNotNull { it.createNewProjectSettingsPanel() } } } - interface SettingsPanel: Disposable { - val data: Settings - fun attach(p: Panel) - fun direnvChanged(state: Boolean) - } - interface SettingsPanelHolder { - val panels: List - } - interface Settings { - fun apply(project: Project) - } - interface ToolchainProvider { - val toolchain: ZigToolchain? - } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt deleted file mode 100644 index d97af0cd..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettings.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.settings - -import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.util.xmlb.annotations.Transient -import kotlin.io.path.isDirectory -import kotlin.io.path.pathString - -data class ZigProjectSettings( - var direnv: Boolean = false, - var overrideStdPath: Boolean = false, - var explicitPathToStd: String? = null, - var toolchainPath: String? = null -): ZigProjectConfigurationProvider.Settings, ZigProjectConfigurationProvider.ToolchainProvider { - override fun apply(project: Project) { - project.zigProjectSettings.loadState(this) - } - - @get:Transient - @set:Transient - override var toolchain: LocalZigToolchain? - get() { - val nioPath = toolchainPath?.toNioPathOrNull() ?: return null - if (!nioPath.isDirectory()) { - return null - } - return LocalZigToolchain(nioPath) - } - set(value) { - toolchainPath = value?.location?.pathString - } -} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt deleted file mode 100644 index 1f3271c4..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsPanel.kt +++ /dev/null @@ -1,211 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.settings - -import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT -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.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.UserDataHolderBase -import com.intellij.openapi.util.io.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.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 ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvider.SettingsPanelHolder, private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel { - private val direnv = JBCheckBox(ZigBrainsBundle.message("settings.project.label.direnv")).apply { addActionListener { - dispatchDirenvUpdate() - } } - private val pathToToolchain = textFieldWithBrowseButton( - project, - FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) - ).also { - it.textField.document.addDocumentListener(object : DocumentAdapter() { - override fun textChanged(e: DocumentEvent) { - dispatchUpdateUI() - } - }) - Disposer.register(this, it) - } - private val toolchainVersion = JBTextArea().also { it.isEditable = false } - private val stdFieldOverride = JBCheckBox(ZigBrainsBundle.message("settings.project.label.override-std")).apply { - addChangeListener { - if (isSelected) { - pathToStd.isEnabled = true - } else { - pathToStd.isEnabled = false - updateUI() - } - } - } - private val pathToStd = textFieldWithBrowseButton( - project, - FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-std")) - ).also { Disposer.register(this, it) } - private var debounce: Job? = null - - private fun dispatchDirenvUpdate() { - holder.panels.forEach { - it.direnvChanged(direnv.isSelected) - } - } - - override fun direnvChanged(state: Boolean) { - dispatchAutodetect(true) - } - - private fun dispatchAutodetect(force: Boolean) { - project.zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) { - withModalProgress(ModalTaskOwner.component(pathToToolchain), "Detecting Zig...", TaskCancellation.cancellable()) { - autodetect(force) - } - } - } - - suspend fun autodetect(force: Boolean) { - if (!force && pathToToolchain.text.isNotBlank()) - return - val data = UserDataHolderBase() - data.putUserData(LocalZigToolchain.DIRENV_KEY, !project.isDefault && direnv.isSelected && DirenvCmd.direnvInstalled()) - val tc = project.suggestZigToolchain(project) ?: return - if (tc !is LocalZigToolchain) { - TODO("Implement non-local zig toolchain in config") - } - if (force || pathToToolchain.text.isBlank()) { - pathToToolchain.text = tc.location.pathString - dispatchUpdateUI() - } - } - - override var data - get() = ZigProjectSettings( - direnv.isSelected, - stdFieldOverride.isSelected, - pathToStd.text.ifBlank { null }, - pathToToolchain.text.ifBlank { null } - ) - set(value) { - direnv.isSelected = value.direnv - pathToToolchain.text = value.toolchainPath ?: "" - stdFieldOverride.isSelected = value.overrideStdPath - pathToStd.text = value.explicitPathToStd ?: "" - pathToStd.isEnabled = value.overrideStdPath - dispatchUpdateUI() - } - - override fun attach(p: Panel): Unit = with(p) { - data = project.zigProjectSettings.state - if (project.isDefault) { - row(ZigBrainsBundle.message("settings.project.label.toolchain")) { - cell(pathToToolchain).resizableColumn().align(AlignX.FILL) - } - row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) { - cell(toolchainVersion) - } - } else { - group(ZigBrainsBundle.message("settings.project.group.title")) { - row(ZigBrainsBundle.message("settings.project.label.toolchain")) { - cell(pathToToolchain).resizableColumn().align(AlignX.FILL) - if (DirenvCmd.direnvInstalled()) { - cell(direnv) - } - } - row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) { - cell(toolchainVersion) - } - row(ZigBrainsBundle.message("settings.project.label.std-location")) { - cell(pathToStd).resizableColumn().align(AlignX.FILL) - cell(stdFieldOverride) - } - } - } - dispatchAutodetect(false) - } - - private fun dispatchUpdateUI() { - debounce?.cancel("New debounce") - debounce = project.zigCoroutineScope.launch { - updateUI() - } - } - - private suspend fun updateUI() { - delay(200) - val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() - if (pathToToolchain == null) { - withEDTContext(ModalityState.any()) { - toolchainVersion.text = "[toolchain path empty or invalid]" - if (!stdFieldOverride.isSelected) { - pathToStd.text = "" - } - } - return - } - val toolchain = LocalZigToolchain(pathToToolchain) - val zig = toolchain.zig - val env = zig.getEnv(project).getOrElse { throwable -> - throwable.printStackTrace() - withEDTContext(ModalityState.any()) { - toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}" - if (!stdFieldOverride.isSelected) { - pathToStd.text = "" - } - } - return - } - val version = env.version - val stdPath = env.stdPath(toolchain, project) - - withEDTContext(ModalityState.any()) { - toolchainVersion.text = version - toolchainVersion.foreground = JBColor.foreground() - if (!stdFieldOverride.isSelected) { - pathToStd.text = stdPath?.pathString ?: "" - } - } - } - - override fun dispose() { - debounce?.cancel("Disposed") - } -} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt deleted file mode 100644 index 71b337d0..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectSettingsService.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.settings - -import com.falsepattern.zigbrains.project.stdlib.ZigSyntheticLibrary -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.openapi.components.* -import com.intellij.openapi.project.Project -import kotlinx.coroutines.launch - -@Service(Service.Level.PROJECT) -@State( - name = "ZigProjectSettings", - storages = [Storage("zigbrains.xml")] -) -class ZigProjectSettingsService(val project: Project): PersistentStateComponent { - @Volatile - private var state = ZigProjectSettings() - - override fun getState(): ZigProjectSettings { - return state.copy() - } - - fun setState(value: ZigProjectSettings) { - this.state = value - zigCoroutineScope.launch { - ZigSyntheticLibrary.reload(project, value) - } - } - - override fun loadState(state: ZigProjectSettings) { - setState(state) - } - - fun isModified(otherData: ZigProjectSettings): Boolean { - return state != otherData - } -} - -val Project.zigProjectSettings get() = service() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt index dd73c2cc..4007c20a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt @@ -23,13 +23,13 @@ package com.falsepattern.zigbrains.project.stdlib import com.falsepattern.zigbrains.Icons -import com.falsepattern.zigbrains.project.settings.ZigProjectSettings -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.navigation.ItemPresentation import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.roots.SyntheticLibrary -import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.refreshAndFindVirtualDirectory import com.intellij.platform.backend.workspace.WorkspaceModel @@ -41,20 +41,20 @@ import java.util.* import javax.swing.Icon class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresentation { - private var state: ZigProjectSettings = project.zigProjectSettings.state.copy() + private var toolchain: ZigToolchain? = ZigToolchainService.getInstance(project).toolchain private val roots by lazy { - runBlocking {getRoot(state, project)}?.let { setOf(it) } ?: emptySet() + runBlocking {getRoot(toolchain, project)}?.let { setOf(it) } ?: emptySet() } private val name by lazy { - getName(state, project) + getName(toolchain, project) } override fun equals(other: Any?): Boolean { if (other !is ZigSyntheticLibrary) return false - return state == other.state + return toolchain == other.toolchain } override fun hashCode(): Int { @@ -76,10 +76,10 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen companion object { private const val ZIG_LIBRARY_ID = "Zig SDK" private const val ZIG_MODULE_ID = "Zig" - suspend fun reload(project: Project, state: ZigProjectSettings) { + suspend fun reload(project: Project, toolchain: ZigToolchain?) { val moduleId = ModuleId(ZIG_MODULE_ID) val workspaceModel = WorkspaceModel.getInstance(project) - val root = getRoot(state, project) ?: return + val root = getRoot(toolchain, project) ?: return val libRoot = LibraryRoot(root.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()), LibraryRootTypeId.SOURCES) val libraryTableId = LibraryTableId.ProjectLibraryTableId val libraryId = LibraryId(ZIG_LIBRARY_ID, libraryTableId) @@ -118,37 +118,39 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen } private fun getName( - state: ZigProjectSettings, + toolchain: ZigToolchain?, project: Project ): String { - val tc = state.toolchain ?: return "Zig" - val version = runBlocking { tc.zig.getEnv(project) }.mapCatching { it.version }.getOrElse { return "Zig" } - return "Zig $version" + val tc = toolchain ?: return "Zig" + toolchain.name?.let { return it } + runBlocking { tc.zig.getEnv(project) } + .mapCatching { it.version } + .getOrNull() + ?.let { return "Zig $it" } + return "Zig" } suspend fun getRoot( - state: ZigProjectSettings, + toolchain: ZigToolchain?, project: Project ): VirtualFile? { - val toolchain = state.toolchain - if (state.overrideStdPath) run { - val ePathStr = state.explicitPathToStd ?: return@run - val ePath = ePathStr.toNioPathOrNull() ?: return@run + //TODO universal + if (toolchain !is LocalZigToolchain) { + return null + } + if (toolchain.std != null) run { + val ePath = toolchain.std if (ePath.isAbsolute) { val roots = ePath.refreshAndFindVirtualDirectory() ?: return@run return roots - } else if (toolchain != null) { - val stdPath = toolchain.location.resolve(ePath) - if (stdPath.isAbsolute) { - val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run - return roots - } + } + val stdPath = toolchain.location.resolve(ePath) + if (stdPath.isAbsolute) { + val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run + return roots } } - if (toolchain != null) { - val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null - val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null - return roots - } - return null + val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null + val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null + return roots } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt index 320118ec..1bf0fd35 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt @@ -22,8 +22,8 @@ package com.falsepattern.zigbrains.project.steps.discovery -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.steps.discovery.ZigStepDiscoveryListener.ErrorType +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.Disposable @@ -76,7 +76,7 @@ class ZigStepDiscoveryService(private val project: Project) { private tailrec suspend fun doReload() { preReload() - val toolchain = project.zigProjectSettings.state.toolchain ?: run { + val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run { errorReload(ErrorType.MissingToolchain) return } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index 1d38ea2d..aaa16dc6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -36,9 +36,19 @@ import java.util.UUID name = "ZigToolchainList", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainListService: SerializablePersistentStateComponent(State()) { +class ZigToolchainListService: SerializablePersistentStateComponent(State()), ZigToolchainListService.IService { private val changeListeners = ArrayList>() - fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { + + override val toolchains: Sequence> + get() = state.toolchains + .asSequence() + .mapNotNull { + val uuid = UUID.fromString(it.key) ?: return@mapNotNull null + val tc = it.value.resolve() ?: return@mapNotNull null + uuid to tc + } + + override fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { val str = uuid.toString() val ref = toolchain.toRef() updateState { @@ -50,7 +60,7 @@ class ZigToolchainListService: SerializablePersistentStateComponent> - get() = state.toolchains - .asSequence() - .mapNotNull { - val uuid = UUID.fromString(it.key) ?: return@mapNotNull null - val tc = it.value.resolve() ?: return@mapNotNull null - uuid to tc - } - data class State( @JvmField val toolchains: Map = emptyMap(), @@ -133,7 +133,18 @@ class ZigToolchainListService: SerializablePersistentStateComponent() + } + + sealed interface IService { + val toolchains: Sequence> + fun setToolchain(uuid: UUID, toolchain: ZigToolchain) + fun registerNewToolchain(toolchain: ZigToolchain): UUID + fun getToolchain(uuid: UUID): ZigToolchain? + fun hasToolchain(uuid: UUID): Boolean + fun removeToolchain(uuid: UUID) + fun addChangeListener(listener: ToolchainListChangeListener) + fun removeChangeListener(listener: ToolchainListChangeListener) } @FunctionalInterface diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt index 5a91afd2..fc517295 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -37,8 +37,8 @@ import java.util.UUID name = "ZigToolchain", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainService: SerializablePersistentStateComponent(State()) { - var toolchainUUID: UUID? +class ZigToolchainService: SerializablePersistentStateComponent(State()), ZigToolchainService.IService { + override var toolchainUUID: UUID? get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { if (ZigToolchainListService.getInstance().hasToolchain(it)) { true @@ -55,7 +55,7 @@ class ZigToolchainService: SerializablePersistentStateComponent() + } + + sealed interface IService { + var toolchainUUID: UUID? + val toolchain: ZigToolchain? } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt index e3cdfc2d..3de6a941 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -25,21 +25,23 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project -import com.intellij.openapi.util.UserDataHolderBase import com.intellij.util.xmlb.annotations.Attribute import com.intellij.util.xmlb.annotations.MapAnnotation import java.nio.file.Path -abstract class ZigToolchain: UserDataHolderBase() { - val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) } +/** + * These MUST be stateless and interchangeable! (e.g., immutable data class) + */ +interface ZigToolchain { + val zig: ZigCompilerTool get() = ZigCompilerTool(this) - abstract val name: String? + val name: String? - abstract fun workingDirectory(project: Project? = null): Path? + fun workingDirectory(project: Project? = null): Path? - abstract suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine + suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine - abstract fun pathToExecutable(toolName: String, project: Project? = null): Path + fun pathToExecutable(toolName: String, project: Project? = null): Path data class Ref( @JvmField 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 a125fe72..fb0a54e5 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 @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.NlsContexts import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.util.minimumWidth import java.util.UUID import javax.swing.JComponent @@ -40,6 +41,8 @@ abstract class ZigToolchainConfigurable( } private var myView: ZigToolchainPanel? = null + var floating: Boolean = false + abstract fun createPanel(): ZigToolchainPanel override fun createOptionsPanel(): JComponent? { @@ -51,7 +54,7 @@ abstract class ZigToolchainConfigurable( } return panel { view.attach(this) - } + }.withMinimumWidth(20) } override fun getEditableObject(): UUID? { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt index 7908af87..ab765d0c 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt @@ -27,9 +27,11 @@ import com.intellij.openapi.Disposable 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 ZigToolchainPanel: Disposable { - private val nameField = JBTextField() + private val nameField = JBTextField(25) protected var nameFieldValue: String? get() = nameField.text.ifBlank { null } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt index e72aea09..91513600 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt @@ -25,6 +25,7 @@ package com.falsepattern.zigbrains.project.toolchain.downloader import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.getSuggestedLocalToolchainPath import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.intellij.icons.AllIcons @@ -52,6 +53,7 @@ import java.util.* import javax.swing.DefaultComboBoxModel import javax.swing.JList import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString object Downloader { suspend fun downloadToolchain(component: Component): ZigToolchain? { @@ -129,7 +131,7 @@ object Downloader { }) var archiveSizeCell: Cell<*>? = null fun detect(item: ZigVersionInfo) { - outputPath.text = System.getProperty("user.home") + "/.zig/" + item.version + outputPath.text = getSuggestedLocalToolchainPath()?.resolve(item.version.rawVersion)?.pathString ?: "" val size = item.dist.size val sizeMb = size / (1024f * 1024f) archiveSizeCell?.comment?.text = ZigBrainsBundle.message("settings.toolchain.downloader.archive-size.text", "%.2fMB".format(sizeMb)) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index 6062afc6..b3073705 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -23,7 +23,6 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine @@ -35,15 +34,17 @@ import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull import java.nio.file.Path -data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain() { +@JvmRecord +data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain { override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } override suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project?): GeneralCommandLine { - if (project != null && (commandLine.getUserData(DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { - commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) - } + //TODO direnv +// if (project != null && (commandLine.getUserData(DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { +// commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) +// } return commandLine } 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 d47568b6..111172c5 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 @@ -59,7 +59,7 @@ class LocalZigToolchainPanel() : ZigToolchainPanel() { Disposer.register(this, it) } private val toolchainVersion = JBTextArea().also { it.isEditable = false } - private val stdFieldOverride = JBCheckBox(ZigBrainsBundle.message("settings.project.label.override-std")).apply { + private val stdFieldOverride = JBCheckBox().apply { addChangeListener { if (isSelected) { pathToStd.isEnabled = true @@ -84,8 +84,8 @@ class LocalZigToolchainPanel() : ZigToolchainPanel() { cell(toolchainVersion) } row(ZigBrainsBundle.message("settings.toolchain.local.std.label")) { - cell(pathToStd).resizableColumn().align(AlignX.FILL) cell(stdFieldOverride) + cell(pathToStd).resizableColumn().align(AlignX.FILL) } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index 4fa6412c..e1ae5f80 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -24,7 +24,6 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider @@ -37,17 +36,23 @@ import com.intellij.openapi.util.text.StringUtil import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes import com.intellij.util.EnvironmentUtil +import com.intellij.util.system.OS import java.io.File +import java.nio.file.Files +import java.nio.file.Path import java.util.UUID +import kotlin.io.path.isDirectory import kotlin.io.path.pathString class LocalZigToolchainProvider: ZigToolchainProvider { override suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): LocalZigToolchain? { - val env = if (project != null && (extraData.getUserData(LocalZigToolchain.DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { - DirenvCmd.importDirenv(project) - } else { - emptyEnv - } + //TODO direnv +// val env = if (project != null && (extraData.getUserData(LocalZigToolchain.DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { +// DirenvCmd.importDirenv(project) +// } else { +// emptyEnv +// } + val env = emptyEnv val zigExePath = env.findExecutableOnPATH("zig") ?: return null return LocalZigToolchain(zigExePath.parent) } @@ -95,6 +100,18 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override fun suggestToolchains(): List { val res = HashSet() EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } + val wellKnown = getWellKnown() + wellKnown.forEach { dir -> + if (!dir.isDirectory()) + return@forEach + runCatching { + Files.newDirectoryStream(dir).use { stream -> + stream.forEach { subDir -> + res.add(subDir.pathString) + } + } + } + } return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) } } @@ -119,6 +136,25 @@ class LocalZigToolchainProvider: ZigToolchainProvider { } } +fun getSuggestedLocalToolchainPath(): Path? { + return getWellKnown().getOrNull(0) +} + +private fun getWellKnown(): 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("zig")) + res.add(xdgDataHome.resolve("zigup")) + } + 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 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 43ef0dc9..e8add1f4 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 @@ -25,183 +25,146 @@ 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.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +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.zigCoroutineScope -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.EDT -import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.ShowSettingsUtil -import com.intellij.openapi.options.newEditor.SettingsDialog -import com.intellij.openapi.options.newEditor.SettingsTreeView import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.DialogWrapper -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.NlsContexts import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.panel import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.awt.event.ItemEvent -import java.lang.reflect.Field -import java.lang.reflect.Method import java.util.UUID -import javax.swing.JComponent +import javax.swing.JButton import kotlin.collections.addAll -class ZigToolchainEditor(private val project: Project): Configurable { - private var myUi: UI? = null - override fun getDisplayName(): @NlsContexts.ConfigurableName String? { - return ZigBrainsBundle.message("settings.toolchain.editor.display-name") +class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubConfigurable, ZigToolchainListService.ToolchainListChangeListener { + private val toolchainBox: TCComboBox + private var selectOnNextReload: UUID? = null + private val model: TCModel + private lateinit var editButton: JButton + init { + model = TCModel(getModelList()) + toolchainBox = TCComboBox(model) + toolchainBox.addItemListener(::itemStateChanged) + ZigToolchainListService.getInstance().addChangeListener(this) } - override fun createComponent(): JComponent? { - if (myUi != null) { - disposeUIResources() + private fun itemStateChanged(event: ItemEvent) { + if (event.stateChange != ItemEvent.SELECTED) { + return } - val ui = UI() - myUi = ui - return panel { - ui.attach(this) - } - } - - override fun isModified(): Boolean { - return myUi?.isModified() == true - } - - override fun apply() { - myUi?.apply() - } - - override fun reset() { - myUi?.reset() - } - - override fun disposeUIResources() { - myUi?.let { Disposer.dispose(it) } - myUi = null - super.disposeUIResources() - } - - inner class UI(): Disposable, ZigToolchainListService.ToolchainListChangeListener { - private val toolchainBox: TCComboBox - private var selectOnNextReload: UUID? = null - private val model: TCModel - init { - model = TCModel(getModelList()) - toolchainBox = TCComboBox(model) - toolchainBox.addItemListener(::itemStateChanged) - ZigToolchainListService.getInstance().addChangeListener(this) - reset() - } - - private fun itemStateChanged(event: ItemEvent) { - if (event.stateChange != ItemEvent.SELECTED) { - return - } - val item = event.item - if (item !is TCListElem.Pseudo) - return - zigCoroutineScope.launch(toolchainBox.asContextElement()) { - val uuid = ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) - withEDTContext(toolchainBox.asContextElement()) { - applyUUIDNowOrOnReload(uuid) - } + val item = event.item + if (item !is TCListElem.Pseudo) + return + zigCoroutineScope.launch(toolchainBox.asContextElement()) { + val uuid = ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) + withEDTContext(toolchainBox.asContextElement()) { + applyUUIDNowOrOnReload(uuid) } } + } - override suspend fun toolchainListChanged() { - withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { - val list = getModelList() - 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 suspend fun toolchainListChanged() { + withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { + val list = getModelList() + 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 } + withContext(Dispatchers.EDT + editButton.asContextElement()) { + editButton.isEnabled = model.selectedItem is TCListElem.Toolchain.Actual + editButton.repaint() + } + } - fun attach(p: Panel): Unit = with(p) { - row(ZigBrainsBundle.message("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()) { - val config = ZigToolchainListEditor() - var inited = false - var selectedUUID: UUID? = toolchainBox.selectedToolchain - config.addItemSelectedListener { - if (inited) { - selectedUUID = it - } - } - val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config) { - config.selectNodeInTree(selectedUUID) - inited = true - } - if (apply) { - applyUUIDNowOrOnReload(selectedUUID) - } + override fun attach(p: Panel): Unit = with(p) { + row(ZigBrainsBundle.message( + if (isForDefaultProject) + "settings.toolchain.editor.toolchain-default.label" + 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 = ZigToolchainListService.getInstance().getToolchain(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 } } + } - @RequiresEdt - private fun applyUUIDNowOrOnReload(uuid: UUID?) { - toolchainBox.selectedToolchain = uuid - if (uuid != null && toolchainBox.selectedToolchain == null) { - selectOnNextReload = uuid - } else { - selectOnNextReload = null - } + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + toolchainBox.selectedToolchain = uuid + if (uuid != null && toolchainBox.selectedToolchain == null) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null } + } - fun isModified(): Boolean { - return ZigToolchainService.getInstance(project).toolchainUUID != toolchainBox.selectedToolchain - } + override fun isModified(context: Project): Boolean { + return ZigToolchainService.getInstance(context).toolchainUUID != toolchainBox.selectedToolchain + } - fun apply() { - ZigToolchainService.getInstance(project).toolchainUUID = toolchainBox.selectedToolchain - } + override fun apply(context: Project) { + ZigToolchainService.getInstance(context).toolchainUUID = toolchainBox.selectedToolchain + } - fun reset() { - toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID - } + override fun reset(context: Project?) { + val project = context ?: ProjectManager.getInstance().defaultProject + toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID + } + override fun dispose() { + ZigToolchainListService.getInstance().removeChangeListener(this) + } + override val newProjectBeforeInitSelector get() = true - override fun dispose() { - ZigToolchainListService.getInstance().removeChangeListener(this) - } + class Adapter(override val context: Project): SubConfigurable.Adapter() { + override fun instantiate() = ZigToolchainEditor(context.isDefault) + override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.editor.display-name") } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/MultiConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/MultiConfigurable.kt deleted file mode 100644 index 36c30c13..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/MultiConfigurable.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.shared - -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.intellij.openapi.options.Configurable -import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.panel -import javax.swing.JComponent - -abstract class MultiConfigurable(val configurables: List): Configurable, ZigProjectConfigurationProvider.SettingsPanelHolder { - final override var panels: List = emptyList() - private set - - override fun createComponent(): JComponent? { - return panel { - panels = configurables.map { it.createComponent(this@MultiConfigurable, this@panel) } - } - } - - override fun isModified(): Boolean { - return configurables.any { it.isModified() } - } - - override fun apply() { - configurables.forEach { it.apply() } - } - - override fun reset() { - configurables.forEach { it.reset() } - } - - override fun disposeUIResources() { - configurables.forEach { Disposer.dispose(it) } - panels = emptyList() - } -} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt index 5e7c52e5..555292e8 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt @@ -22,16 +22,56 @@ package com.falsepattern.zigbrains.shared -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.SettingsPanelHolder import com.intellij.openapi.Disposable -import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.options.Configurable +import com.intellij.openapi.util.Disposer import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.dsl.builder.panel +import javax.swing.JComponent -interface SubConfigurable: Disposable { - fun createComponent(holder: SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel - fun isModified(): Boolean - @Throws(ConfigurationException::class) - fun apply() - fun reset() +interface SubConfigurable: Disposable { + fun attach(panel: Panel) + fun isModified(context: T): Boolean + fun apply(context: T) + fun reset(context: T?) + + val newProjectBeforeInitSelector: Boolean get() = false + + abstract class Adapter: Configurable { + private var myConfigurable: SubConfigurable? = null + + abstract fun instantiate(): SubConfigurable + protected abstract val context: T + + override fun createComponent(): JComponent? { + if (myConfigurable != null) { + disposeUIResources() + } + val configurable = instantiate() + configurable.reset(context) + myConfigurable = configurable + return panel { + configurable.attach(this) + } + } + + override fun isModified(): Boolean { + return myConfigurable?.isModified(context) == true + } + + override fun apply() { + myConfigurable?.apply(context) + } + + override fun reset() { + myConfigurable?.reset(context) + } + + override fun disposeUIResources() { + val configurable = myConfigurable + myConfigurable = null + configurable?.let { Disposer.dispose(it) } + super.disposeUIResources() + } + } } \ No newline at end of file diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index 8e51f2ab..2c19853c 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -140,7 +140,7 @@ /> diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index 7c761c44..00957a4b 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -116,6 +116,7 @@ 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= @@ -148,4 +149,4 @@ settings.toolchain.local-selector.chooser.title=Existing Zig Install Directory settings.toolchain.local-selector.state.invalid=Invalid toolchain path settings.toolchain.local-selector.state.already-exists-unnamed=Toolchain already exists settings.toolchain.local-selector.state.already-exists-named=Toolchain already exists as "{0}" -settings.toolchain.local-selector.state.ok=OK \ No newline at end of file +settings.toolchain.local-selector.state.ok=OK diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt index 5a1b423a..63228b94 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt @@ -34,7 +34,7 @@ class ZLSProjectConfigurationProvider: ZigProjectConfigurationProvider { startLSP(project, true) } - override fun createConfigurable(project: Project): SubConfigurable { + override fun createConfigurable(project: Project): SubConfigurable { return ZLSSettingsConfigurable(project) } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 0c1edf2b..78171fe4 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -4,7 +4,7 @@ FalsePattern com.intellij.modules.platform - com.redhat.devtools.lsp4ij + com.redhat.devtools.lsp4ij com.intellij.modules.cidr.debugger com.intellij.cidr.base com.intellij.clion From 54cd5142493048c2e960d4d8259e046ea787535d Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Wed, 9 Apr 2025 18:29:06 +0200 Subject: [PATCH 11/22] local zig toolchain name, edit button state, sorted recommends --- .../toolchain/base/ZigToolchainProvider.kt | 8 +-- .../toolchain/downloader/Downloader.kt | 2 +- .../toolchain/downloader/LocalSelector.kt | 67 +++++++++++++------ .../toolchain/local/LocalZigToolchain.kt | 18 +++-- .../local/LocalZigToolchainProvider.kt | 3 +- .../toolchain/ui/ZigToolchainEditor.kt | 20 ++++-- .../resources/zigbrains/Bundle.properties | 3 +- 7 files changed, 79 insertions(+), 42 deletions(-) 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 43f3df41..fb960bb0 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 @@ -25,9 +25,9 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.UserDataHolder import com.intellij.ui.SimpleColoredComponent +import com.intellij.util.text.SemVer import java.util.UUID private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") @@ -41,7 +41,7 @@ internal interface ZigToolchainProvider { fun serialize(toolchain: ZigToolchain): Map fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> - fun suggestToolchains(): List + fun suggestToolchains(): List> fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean) } @@ -71,8 +71,8 @@ fun suggestZigToolchains(): List { return EXTENSION_POINT_NAME.extensionList.flatMap { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } val suggestions = ext.suggestToolchains() - suggestions.filter { suggestion -> compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } } - } + suggestions.filter { suggestion -> compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion.second) } } + }.sortedByDescending { it.first }.map { it.second } } fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt index 91513600..8bde14f2 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt @@ -74,7 +74,7 @@ object Downloader { ) { version.downloadAndUnpack(downloadPath) } - return LocalZigToolchain.tryFromPath(downloadPath) + return LocalZigToolchain.tryFromPath(downloadPath)?.second } @RequiresEdt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt index 900006da..3ebea410 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt @@ -30,11 +30,13 @@ import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.intellij.icons.AllIcons +import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.util.Disposer import com.intellij.ui.DocumentAdapter import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.panel @@ -52,39 +54,50 @@ object LocalSelector { @RequiresEdt private fun doBrowseFromDisk(): ZigToolchain? { val dialog = DialogBuilder() + val name = JBTextField().also { it.columns = 25 } val path = textFieldWithBrowseButton( null, FileChooserDescriptorFactory.createSingleFolderDescriptor() .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")) ) Disposer.register(dialog, path) - path.textField.columns = 50 lateinit var errorMessageBox: JBLabel + fun verify(path: String) { + val tc = LocalZigToolchain.tryFromPathString(path)?.second + if (tc == null) { + errorMessageBox.icon = AllIcons.General.Error + errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") + dialog.setOkActionEnabled(false) + } else if (ZigToolchainListService + .getInstance() + .toolchains + .mapNotNull { it.second as? LocalZigToolchain } + .any { it.location == tc.location } + ) { + errorMessageBox.icon = AllIcons.General.Warning + errorMessageBox.text = tc.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } + ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") + dialog.setOkActionEnabled(true) + } else { + errorMessageBox.icon = AllIcons.General.Information + errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") + dialog.setOkActionEnabled(true) + } + val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() + name.emptyText.text = tc?.name ?: "" + if (prevNameDefault) { + name.text = name.emptyText.text + } + } path.addDocumentListener(object: DocumentAdapter() { override fun textChanged(e: DocumentEvent) { - val tc = LocalZigToolchain.tryFromPathString(path.text) - if (tc == null) { - errorMessageBox.icon = AllIcons.General.Error - errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") - dialog.setOkActionEnabled(false) - } else if (ZigToolchainListService - .getInstance() - .toolchains - .mapNotNull { it.second as? LocalZigToolchain } - .any { it.location == tc.location } - ) { - errorMessageBox.icon = AllIcons.General.Warning - errorMessageBox.text = tc.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } - ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") - dialog.setOkActionEnabled(true) - } else { - errorMessageBox.icon = Icons.Zig - errorMessageBox.text = tc.name ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") - dialog.setOkActionEnabled(true) - } + verify(path.text) } }) val center = panel { + row(ZigBrainsBundle.message("settings.toolchain.local-selector.name.label")) { + cell(name).resizableColumn().align(AlignX.FILL) + } row(ZigBrainsBundle.message("settings.toolchain.local-selector.path.label")) { cell(path).resizableColumn().align(AlignX.FILL) } @@ -97,9 +110,19 @@ object LocalSelector { dialog.setTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.title")) dialog.addCancelAction() dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.toolchain.local-selector.ok-action")) } + val chosenFile = FileChooser.chooseFile( + FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")), + null, + null + ) + if (chosenFile != null) { + verify(chosenFile.path) + path.text = chosenFile.path + } if (!dialog.showAndGet()) { return null } - return LocalZigToolchain.tryFromPathString(path.text) + return LocalZigToolchain.tryFromPathString(path.text)?.second?.also { it.copy(name = name.text.ifBlank { null } ?: it.name) } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index b3073705..66a2a9a1 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -32,6 +32,7 @@ import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull +import com.intellij.util.text.SemVer import java.nio.file.Path @JvmRecord @@ -66,22 +67,27 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override } } - fun tryFromPathString(pathStr: String?): LocalZigToolchain? { + fun tryFromPathString(pathStr: String?): Pair? { return pathStr?.ifBlank { null }?.toNioPathOrNull()?.let(::tryFromPath) } - fun tryFromPath(path: Path): LocalZigToolchain? { + fun tryFromPath(path: Path): Pair? { var tc = LocalZigToolchain(path) if (!tc.zig.fileValid()) { return null } - tc.zig + val versionStr = tc.zig .getEnvBlocking(null) .getOrNull() ?.version - ?.let { "Zig $it" } - ?.let { tc = tc.copy(name = it) } - return tc + val version: SemVer? + if (versionStr != null) { + version = SemVer.parseFromText(versionStr) + tc = tc.copy(name = "Zig $versionStr") + } else { + version = null + } + return version to tc } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index e1ae5f80..dc6e54ce 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -37,6 +37,7 @@ import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes import com.intellij.util.EnvironmentUtil import com.intellij.util.system.OS +import com.intellij.util.text.SemVer import java.io.File import java.nio.file.Files import java.nio.file.Path @@ -97,7 +98,7 @@ class LocalZigToolchainProvider: ZigToolchainProvider { return LocalZigToolchainConfigurable(uuid, toolchain) } - override fun suggestToolchains(): List { + override fun suggestToolchains(): List> { val res = HashSet() EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } val wellKnown = getWellKnown() 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 e8add1f4..99c6bcd1 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 @@ -52,7 +52,7 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC private val toolchainBox: TCComboBox private var selectOnNextReload: UUID? = null private val model: TCModel - private lateinit var editButton: JButton + private var editButton: JButton? = null init { model = TCModel(getModelList()) toolchainBox = TCComboBox(model) @@ -60,15 +60,21 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC ZigToolchainListService.getInstance().addChangeListener(this) } + 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 = ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) + val uuid = runCatching { ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) }.getOrNull() withEDTContext(toolchainBox.asContextElement()) { applyUUIDNowOrOnReload(uuid) } @@ -105,10 +111,6 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC } model.selectedItem = TCListElem.None } - withContext(Dispatchers.EDT + editButton.asContextElement()) { - editButton.isEnabled = model.selectedItem is TCListElem.Toolchain.Actual - editButton.repaint() - } } override fun attach(p: Panel): Unit = with(p) { @@ -129,7 +131,10 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC applyUUIDNowOrOnReload(selectedUUID) } } - }.component.let { editButton = it } + }.component.let { + editButton = it + refreshButtonState(toolchainBox.selectedItem) + } } } @@ -154,6 +159,7 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC override fun reset(context: Project?) { val project = context ?: ProjectManager.getInstance().defaultProject toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID + refreshButtonState(toolchainBox.selectedItem) } override fun dispose() { diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index 00957a4b..0dfb36b3 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -143,10 +143,11 @@ settings.toolchain.downloader.archive-size.text=Archive size: {0} settings.toolchain.downloader.service.index=Zig version information settings.toolchain.downloader.service.tarball=Zig archive settings.toolchain.local-selector.title=Select Zig From Disk +settings.toolchain.local-selector.name.label=Name: settings.toolchain.local-selector.path.label=Path: settings.toolchain.local-selector.ok-action=Add settings.toolchain.local-selector.chooser.title=Existing Zig Install Directory settings.toolchain.local-selector.state.invalid=Invalid toolchain path settings.toolchain.local-selector.state.already-exists-unnamed=Toolchain already exists settings.toolchain.local-selector.state.already-exists-named=Toolchain already exists as "{0}" -settings.toolchain.local-selector.state.ok=OK +settings.toolchain.local-selector.state.ok=Toolchain path OK From 12e5ffdea1b066dfa03a438ed339ab8f9261c890 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Wed, 9 Apr 2025 22:35:44 +0200 Subject: [PATCH 12/22] finishing touches for the toolchain selector --- .../toolchain/ZigToolchainListService.kt | 36 +++++++++---------- .../project/toolchain/ZigToolchainService.kt | 12 +++---- .../toolchain/base/ZigToolchainProvider.kt | 8 ++--- .../local/LocalZigToolchainProvider.kt | 15 ++++++-- .../toolchain/ui/ZigToolchainEditor.kt | 3 +- .../toolchain/ui/ZigToolchainListEditor.kt | 3 +- .../zigbrains/project/toolchain/ui/model.kt | 2 +- 7 files changed, 45 insertions(+), 34 deletions(-) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index aaa16dc6..e18e1070 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -36,7 +36,7 @@ import java.util.UUID name = "ZigToolchainList", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainListService: SerializablePersistentStateComponent(State()), ZigToolchainListService.IService { +class ZigToolchainListService: SerializablePersistentStateComponent(State()), IZigToolchainListService { private val changeListeners = ArrayList>() override val toolchains: Sequence> @@ -133,22 +133,22 @@ class ZigToolchainListService: SerializablePersistentStateComponent() - } - - sealed interface IService { - val toolchains: Sequence> - fun setToolchain(uuid: UUID, toolchain: ZigToolchain) - fun registerNewToolchain(toolchain: ZigToolchain): UUID - fun getToolchain(uuid: UUID): ZigToolchain? - fun hasToolchain(uuid: UUID): Boolean - fun removeToolchain(uuid: UUID) - fun addChangeListener(listener: ToolchainListChangeListener) - fun removeChangeListener(listener: ToolchainListChangeListener) - } - - @FunctionalInterface - interface ToolchainListChangeListener { - suspend fun toolchainListChanged() + fun getInstance(): IZigToolchainListService = service() } } + +@FunctionalInterface +interface ToolchainListChangeListener { + suspend fun toolchainListChanged() +} + +sealed interface IZigToolchainListService { + val toolchains: Sequence> + fun setToolchain(uuid: UUID, toolchain: ZigToolchain) + fun registerNewToolchain(toolchain: ZigToolchain): UUID + fun getToolchain(uuid: UUID): ZigToolchain? + fun hasToolchain(uuid: UUID): Boolean + fun removeToolchain(uuid: UUID) + fun addChangeListener(listener: ToolchainListChangeListener) + fun removeChangeListener(listener: ToolchainListChangeListener) +} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt index fc517295..5604af3b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -37,7 +37,7 @@ import java.util.UUID name = "ZigToolchain", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainService: SerializablePersistentStateComponent(State()), ZigToolchainService.IService { +class ZigToolchainService: SerializablePersistentStateComponent(State()), IZigToolchainService { override var toolchainUUID: UUID? get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { if (ZigToolchainListService.getInstance().hasToolchain(it)) { @@ -66,11 +66,11 @@ class ZigToolchainService: SerializablePersistentStateComponent() + fun getInstance(project: Project): IZigToolchainService = project.service() } +} - sealed interface IService { - var toolchainUUID: UUID? - val toolchain: ZigToolchain? - } +sealed interface IZigToolchainService { + var toolchainUUID: UUID? + val toolchain: ZigToolchain? } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt index fb960bb0..841d0bf8 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 @@ -42,7 +42,7 @@ internal interface ZigToolchainProvider { fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> fun suggestToolchains(): List> - fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean) + fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) } fun ZigToolchain.Ref.resolve(): ZigToolchain? { @@ -67,7 +67,7 @@ fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<* } fun suggestZigToolchains(): List { - val existing = ZigToolchainListService.getInstance().toolchains.map { (uuid, tc) -> tc }.toList() + val existing = ZigToolchainListService.getInstance().toolchains.map { (_, tc) -> tc }.toList() return EXTENSION_POINT_NAME.extensionList.flatMap { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } val suggestions = ext.suggestToolchains() @@ -75,7 +75,7 @@ fun suggestZigToolchains(): List { }.sortedByDescending { it.first }.map { it.second } } -fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean) { +fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return provider.render(this, component, isSuggestion) + return provider.render(this, component, isSuggestion, isSelected) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index dc6e54ce..7d550c57 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -116,24 +116,33 @@ class LocalZigToolchainProvider: ZigToolchainProvider { return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) } } - override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean) { + override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { toolchain as LocalZigToolchain - val path = presentDetectedPath(toolchain.location.pathString) val name = toolchain.name + val path = presentDetectedPath(toolchain.location.pathString) val primary: String val secondary: String? + val tooltip: String? if (isSuggestion) { primary = path secondary = name + tooltip = null } else { primary = name ?: "Zig" - secondary = path + if (isSelected) { + secondary = null + tooltip = path + } else { + secondary = path + tooltip = null + } } component.append(primary) if (secondary != null) { component.append(" ") component.append(secondary, SimpleTextAttributes.GRAYED_ATTRIBUTES) } + component.toolTipText = tooltip } } 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 99c6bcd1..265155dd 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -23,6 +23,7 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable @@ -48,7 +49,7 @@ import java.util.UUID import javax.swing.JButton import kotlin.collections.addAll -class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubConfigurable, ZigToolchainListService.ToolchainListChangeListener { +class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubConfigurable, ToolchainListChangeListener { private val toolchainBox: TCComboBox private var selectOnNextReload: UUID? = null private val model: TCModel diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index 766843d5..0037de9a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable @@ -62,7 +63,7 @@ import javax.swing.JComponent import javax.swing.event.DocumentEvent import javax.swing.tree.DefaultTreeModel -class ZigToolchainListEditor : MasterDetailsComponent(), ZigToolchainListService.ToolchainListChangeListener { +class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeListener { private var isTreeInitialized = false private var registered: Boolean = false private var itemSelectedListeners = ArrayList>() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index a77b1195..0671536b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -204,7 +204,7 @@ internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRend } this.icon = icon val toolchain = value.toolchain - toolchain.render(this, isSuggestion) + toolchain.render(this, isSuggestion, index == -1) } is TCListElem.Download -> { From c7e33ea8deba8460780c039500be284cf8664a62 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Wed, 9 Apr 2025 23:20:40 +0200 Subject: [PATCH 13/22] modular project configurables --- .../project/newproject/ZigNewProjectPanel.kt | 2 +- ...gurationProvider.kt => ZigConfigurable.kt} | 20 +++------ .../ZigProjectConfigurationProvider.kt | 17 ++----- .../local/LocalZigToolchainProvider.kt | 14 ++++++ .../toolchain/ui/ZigToolchainEditor.kt | 11 +++-- .../toolchain/ui/ZigToolchainListEditor.kt | 35 --------------- .../zigbrains/shared/SubConfigurable.kt | 45 +++++++++++++------ .../resources/META-INF/zigbrains-core.xml | 10 +++-- .../resources/zigbrains/Bundle.properties | 1 + 9 files changed, 71 insertions(+), 84 deletions(-) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/{ZigCoreProjectConfigurationProvider.kt => ZigConfigurable.kt} (63%) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt index 96eee5e2..2f0cc95e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/newproject/ZigNewProjectPanel.kt @@ -41,7 +41,7 @@ import javax.swing.ListSelectionModel class ZigNewProjectPanel(private var handleGit: Boolean): Disposable { private val git = JBCheckBox() - val panels = ZigProjectConfigurationProvider.createNewProjectSettingsPanels().onEach { Disposer.register(this, it) } + val panels = ZigProjectConfigurationProvider.createPanels(null).onEach { Disposer.register(this, it) } private val templateList = JBList(JBList.createDefaultListModel(defaultTemplates)).apply { selectionMode = ListSelectionModel.SINGLE_SELECTION selectedIndex = 0 diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt similarity index 63% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt index 47a30bec..ba957bc4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigCoreProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt @@ -22,23 +22,15 @@ package com.falsepattern.zigbrains.project.settings -import com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainEditor +import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project +import com.intellij.openapi.util.NlsContexts -class ZigCoreProjectConfigurationProvider: ZigProjectConfigurationProvider { - override fun handleMainConfigChanged(project: Project) { +class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter() { + override fun instantiate(): List> { + return ZigProjectConfigurationProvider.createPanels(context) } - override fun createConfigurable(project: Project): Configurable { - return ZigToolchainEditor.Adapter(project) - } - - override fun createNewProjectSettingsPanel(): SubConfigurable { - return ZigToolchainEditor().also { it.reset(null) } - } - - override val priority: Int - get() = 0 + override fun getDisplayName() = ZigBrainsBundle.message("settings.project.display-name") } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt index 9932c3a7..29dbc1e3 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt @@ -24,24 +24,15 @@ package com.falsepattern.zigbrains.project.settings import com.falsepattern.zigbrains.shared.SubConfigurable import com.intellij.openapi.extensions.ExtensionPointName -import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.Project interface ZigProjectConfigurationProvider { - fun handleMainConfigChanged(project: Project) - fun createConfigurable(project: Project): Configurable - fun createNewProjectSettingsPanel(): SubConfigurable? - val priority: Int + fun create(project: Project?): SubConfigurable? + val index: Int companion object { private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.projectConfigProvider") - fun mainConfigChanged(project: Project) { - EXTENSION_POINT_NAME.extensionList.forEach { it.handleMainConfigChanged(project) } - } - fun createConfigurables(project: Project): List { - return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.map { it.createConfigurable(project) } - } - fun createNewProjectSettingsPanels(): List> { - return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.mapNotNull { it.createNewProjectSettingsPanel() } + fun createPanels(project: Project?): List> { + return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(project) } } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index 7d550c57..4b12e449 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -150,6 +150,20 @@ fun getSuggestedLocalToolchainPath(): Path? { return getWellKnown().getOrNull(0) } +/** + * Returns the paths to the following list of folders: + * + * 1. DATA/zig + * 2. DATA/zigup + * 3. 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 getWellKnown(): List { val home = System.getProperty("user.home")?.toNioPathOrNull() ?: return emptyList() val xdgDataHome = when(OS.CURRENT) { 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 265155dd..f21fab92 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -23,6 +23,7 @@ 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.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService @@ -169,9 +170,13 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC override val newProjectBeforeInitSelector get() = true - class Adapter(override val context: Project): SubConfigurable.Adapter() { - override fun instantiate() = ZigToolchainEditor(context.isDefault) - override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.editor.display-name") + class Provider: ZigProjectConfigurationProvider { + override fun create(project: Project?): SubConfigurable? { + return ZigToolchainEditor(project?.isDefault ?: false).also { it.reset(project) } + } + + override val index: Int get() = 0 + } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index 0037de9a..b2db35e1 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -22,57 +22,30 @@ package com.falsepattern.zigbrains.project.toolchain.ui -import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains -import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader -import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.Presentation -import com.intellij.openapi.application.EDT -import com.intellij.openapi.components.Service -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.MasterDetailsComponent -import com.intellij.openapi.ui.NamedConfigurable -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.ui.DocumentAdapter -import com.intellij.ui.components.JBLabel -import com.intellij.ui.components.textFieldWithBrowseButton -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.panel -import com.intellij.util.Consumer import com.intellij.util.IconUtil import com.intellij.util.asSafely -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.util.UUID import javax.swing.JComponent -import javax.swing.event.DocumentEvent import javax.swing.tree.DefaultTreeModel class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeListener { private var isTreeInitialized = false private var registered: Boolean = false - private var itemSelectedListeners = ArrayList>() - - fun addItemSelectedListener(c: Consumer) { - synchronized(itemSelectedListeners) { - itemSelectedListeners.add(c) - } - } override fun createComponent(): JComponent { if (!isTreeInitialized) { @@ -102,14 +75,6 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList return listOf(add, MyDeleteAction()) } - override fun updateSelection(configurable: NamedConfigurable<*>?) { - super.updateSelection(configurable) - val uuid = configurable?.editableObject as? UUID - synchronized(itemSelectedListeners) { - itemSelectedListeners.forEach { it.consume(uuid) } - } - } - override fun onItemDeleted(item: Any?) { if (item is UUID) { ZigToolchainListService.getInstance().removeToolchain(item) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt index 555292e8..99397c2a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/SubConfigurable.kt @@ -27,6 +27,7 @@ import com.intellij.openapi.options.Configurable import com.intellij.openapi.util.Disposer import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.panel +import java.util.ArrayList import javax.swing.JComponent interface SubConfigurable: Disposable { @@ -38,40 +39,56 @@ interface SubConfigurable: Disposable { val newProjectBeforeInitSelector: Boolean get() = false abstract class Adapter: Configurable { - private var myConfigurable: SubConfigurable? = null + private val myConfigurables: MutableList> = ArrayList() - abstract fun instantiate(): SubConfigurable + abstract fun instantiate(): List> protected abstract val context: T override fun createComponent(): JComponent? { - if (myConfigurable != null) { - disposeUIResources() + val configurables: List> + synchronized(myConfigurables) { + if (myConfigurables.isEmpty()) { + disposeConfigurables() + } + configurables = instantiate() + configurables.forEach { it.reset(context) } + myConfigurables.clear() + myConfigurables.addAll(configurables) } - val configurable = instantiate() - configurable.reset(context) - myConfigurable = configurable return panel { - configurable.attach(this) + configurables.forEach { it.attach(this) } } } override fun isModified(): Boolean { - return myConfigurable?.isModified(context) == true + synchronized(myConfigurables) { + return myConfigurables.any { it.isModified(context) } + } } override fun apply() { - myConfigurable?.apply(context) + synchronized(myConfigurables) { + myConfigurables.forEach { it.apply(context) } + } } override fun reset() { - myConfigurable?.reset(context) + synchronized(myConfigurables) { + myConfigurables.forEach { it.reset(context) } + } } override fun disposeUIResources() { - val configurable = myConfigurable - myConfigurable = null - configurable?.let { Disposer.dispose(it) } + synchronized(myConfigurables) { + disposeConfigurables() + } super.disposeUIResources() } + + private fun disposeConfigurables() { + val configurables = ArrayList(myConfigurables) + myConfigurables.clear() + configurables.forEach { Disposer.dispose(it) } + } } } \ No newline at end of file diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index 2c19853c..8fca23cc 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -140,15 +140,17 @@ /> diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index 0dfb36b3..d3fe1d93 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -110,6 +110,7 @@ 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.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 From 3ceb61f2ddd70bda39fa13ff5555546cf8a2d3b7 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 01:44:43 +0200 Subject: [PATCH 14/22] async toolchain resolution and other tweaks --- .../com/falsepattern/zigbrains/ZBStartup.kt | 1 - .../zigbrains/direnv/DirenvProjectService.kt | 35 ------- .../direnv/{DirenvCmd.kt => DirenvService.kt} | 42 +++++---- .../com/falsepattern/zigbrains/direnv/Env.kt | 7 +- .../project/execution/base/ZigExecConfig.kt | 1 - .../toolchain/ZigToolchainListService.kt | 13 +++ .../project/toolchain/base/ZigToolchain.kt | 5 + .../toolchain/base/ZigToolchainProvider.kt | 20 +++- .../toolchain/downloader/Downloader.kt | 2 +- .../toolchain/downloader/LocalSelector.kt | 91 +++++++++++++------ .../toolchain/local/LocalZigToolchain.kt | 22 ++--- .../local/LocalZigToolchainProvider.kt | 14 +-- .../toolchain/tools/ZigCompilerTool.kt | 2 - .../ui/ZigToolchainComboBoxHandler.kt | 2 +- .../toolchain/ui/ZigToolchainEditor.kt | 4 +- .../toolchain/ui/ZigToolchainListEditor.kt | 8 +- .../project/toolchain/ui/elements.kt | 9 +- .../zigbrains/project/toolchain/ui/model.kt | 71 ++++++++++++++- .../zigbrains/shared/ipc/IPCUtil.kt | 6 +- 19 files changed, 230 insertions(+), 125 deletions(-) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvProjectService.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/direnv/{DirenvCmd.kt => DirenvService.kt} (80%) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt index 5d6a1d7f..88e78369 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt @@ -22,7 +22,6 @@ package com.falsepattern.zigbrains -import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain import com.intellij.ide.BrowserUtil diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvProjectService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvProjectService.kt deleted file mode 100644 index fec31d4a..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvProjectService.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.direnv - -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project -import kotlinx.coroutines.sync.Mutex - -@Service(Service.Level.PROJECT) -class DirenvProjectService { - val mutex = Mutex() -} - -val Project.direnvService get() = service() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvCmd.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt similarity index 80% rename from core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvCmd.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt index 6c361bae..282b3783 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvCmd.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt @@ -29,23 +29,29 @@ import com.intellij.ide.impl.isTrusted import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.notification.Notifications +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.platform.util.progress.withProgressText import com.intellij.util.io.awaitExit import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.nio.file.Path -object DirenvCmd { - suspend fun importDirenv(project: Project): Env { - if (!direnvInstalled() || !project.isTrusted()) - return emptyEnv - val workDir = project.guessProjectDir()?.toNioPath() ?: return emptyEnv +@Service(Service.Level.PROJECT) +class DirenvService(val project: Project) { + val mutex = Mutex() - val runOutput = run(project, workDir, "export", "json") + suspend fun import(): Env { + if (!isInstalled || !project.isTrusted() || project.isDefault) + return Env.empty + val workDir = project.guessProjectDir()?.toNioPath() ?: return Env.empty + + val runOutput = run(workDir, "export", "json") if (runOutput.error) { if (runOutput.output.contains("is blocked")) { Notifications.Bus.notify(Notification( @@ -54,7 +60,7 @@ object DirenvCmd { ZigBrainsBundle.message("notification.content.direnv-blocked"), NotificationType.ERROR )) - return emptyEnv + return Env.empty } else { Notifications.Bus.notify(Notification( GROUP_DISPLAY_ID, @@ -62,22 +68,22 @@ object DirenvCmd { ZigBrainsBundle.message("notification.content.direnv-error", runOutput.output), NotificationType.ERROR )) - return emptyEnv + return Env.empty } } return if (runOutput.output.isBlank()) { - emptyEnv + Env.empty } else { Env(Json.decodeFromString>(runOutput.output)) } } - private suspend fun run(project: Project, workDir: Path, vararg args: String): DirenvOutput { + private suspend fun run(workDir: Path, vararg args: String): DirenvOutput { val cli = GeneralCommandLine("direnv", *args).withWorkingDirectory(workDir) val (process, exitCode) = withProgressText("Running ${cli.commandLineString}") { withContext(Dispatchers.IO) { - project.direnvService.mutex.withLock { + mutex.withLock { val process = cli.createProcess() val exitCode = process.awaitExit() process to exitCode @@ -94,17 +100,13 @@ object DirenvCmd { return DirenvOutput(stdOut, false) } - private const val GROUP_DISPLAY_ID = "zigbrains-direnv" - - private val _direnvInstalled by lazy { + val isInstalled: Boolean by lazy { // Using the builtin stuff here instead of Env because it should only scan for direnv on the process path PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("direnv") != null } - fun direnvInstalled() = _direnvInstalled -} -suspend fun Project?.getDirenv(): Env { - if (this == null) - return emptyEnv - return DirenvCmd.importDirenv(this) + companion object { + private const val GROUP_DISPLAY_ID = "zigbrains-direnv" + fun getInstance(project: Project): DirenvService = project.service() + } } \ No newline at end of file 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 4d695819..2af970ac 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/Env.kt @@ -34,6 +34,7 @@ import kotlin.io.path.isDirectory import kotlin.io.path.isExecutable import kotlin.io.path.isRegularFile +@JvmRecord data class Env(val env: Map) { private val path get() = getVariable("PATH")?.split(File.pathSeparatorChar) @@ -55,6 +56,8 @@ data class Env(val env: Map) { emit(exePath) } } -} -val emptyEnv = Env(emptyMap()) \ No newline at end of file + companion object { + val empty = Env(emptyMap()) + } +} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt index f9269bc9..cf01992c 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt @@ -22,7 +22,6 @@ package com.falsepattern.zigbrains.project.execution.base -import com.falsepattern.zigbrains.direnv.DirenvCmd import com.intellij.execution.ExecutionException import com.intellij.execution.Executor import com.intellij.execution.configurations.ConfigurationFactory diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index e18e1070..8db422bd 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -109,6 +109,18 @@ class ZigToolchainListService: SerializablePersistentStateComponent withUniqueName(toolchain: T): T { + val baseName = toolchain.name ?: "" + var index = 0 + var currentName = baseName + while (toolchains.any { (_, existing) -> existing.name == currentName }) { + index++ + currentName = "$baseName ($index)" + } + @Suppress("UNCHECKED_CAST") + return toolchain.withName(currentName) as T + } + private fun notifyChanged() { synchronized(changeListeners) { var i = 0 @@ -151,4 +163,5 @@ sealed interface IZigToolchainListService { fun removeToolchain(uuid: UUID) fun addChangeListener(listener: ToolchainListChangeListener) fun removeChangeListener(listener: ToolchainListChangeListener) + fun withUniqueName(toolchain: T): T } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt index 3de6a941..23aa9122 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -43,6 +43,11 @@ interface ZigToolchain { fun pathToExecutable(toolName: String, project: Project? = null): Path + /** + * Returned object must be the same class. + */ + fun withName(newName: String?): ZigToolchain + data class Ref( @JvmField @Attribute diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt index 841d0bf8..e06f6f8d 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 @@ -23,11 +23,14 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.util.UserDataHolder import com.intellij.ui.SimpleColoredComponent import com.intellij.util.text.SemVer +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import java.util.UUID private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") @@ -41,7 +44,7 @@ internal interface ZigToolchainProvider { fun serialize(toolchain: ZigToolchain): Map fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> - fun suggestToolchains(): List> + fun suggestToolchains(): List> fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) } @@ -66,13 +69,22 @@ fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<* return provider.createConfigurable(uuid, this) } -fun suggestZigToolchains(): List { +fun suggestZigToolchains(): List> { val existing = ZigToolchainListService.getInstance().toolchains.map { (_, tc) -> tc }.toList() return EXTENSION_POINT_NAME.extensionList.flatMap { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } val suggestions = ext.suggestToolchains() - suggestions.filter { suggestion -> compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion.second) } } - }.sortedByDescending { it.first }.map { it.second } + suggestions.map { suggestion -> + zigCoroutineScope.async { + val sugg = suggestion.await() + if (compatibleExisting.none { existing -> ext.matchesSuggestion(existing, sugg) }) { + sugg + } else { + throw IllegalArgumentException() + } + } + } + } } fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt index 8bde14f2..6183b3a0 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt @@ -74,7 +74,7 @@ object Downloader { ) { version.downloadAndUnpack(downloadPath) } - return LocalZigToolchain.tryFromPath(downloadPath)?.second + return LocalZigToolchain.tryFromPath(downloadPath)?.let { LocalSelector.browseFromDisk(component, it) } } @RequiresEdt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt index 3ebea410..fde0e7da 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt @@ -28,12 +28,19 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.util.Disposer +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.components.JBLabel import com.intellij.ui.components.JBTextField @@ -42,17 +49,19 @@ import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.panel import com.intellij.util.concurrency.annotations.RequiresEdt import java.awt.Component +import java.util.concurrent.atomic.AtomicBoolean import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString object LocalSelector { - suspend fun browseFromDisk(component: Component): ZigToolchain? { - return runInterruptibleEDT(component.asContextElement()) { - doBrowseFromDisk() + suspend fun browseFromDisk(component: Component, preSelected: LocalZigToolchain? = null): ZigToolchain? { + return withEDTContext(component.asContextElement()) { + doBrowseFromDisk(component, preSelected) } } @RequiresEdt - private fun doBrowseFromDisk(): ZigToolchain? { + private suspend fun doBrowseFromDisk(component: Component, preSelected: LocalZigToolchain?): ZigToolchain? { val dialog = DialogBuilder() val name = JBTextField().also { it.columns = 25 } val path = textFieldWithBrowseButton( @@ -62,26 +71,31 @@ object LocalSelector { ) Disposer.register(dialog, path) lateinit var errorMessageBox: JBLabel - fun verify(path: String) { - val tc = LocalZigToolchain.tryFromPathString(path)?.second + fun verify(tc: LocalZigToolchain?) { + var tc = tc if (tc == null) { errorMessageBox.icon = AllIcons.General.Error errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") dialog.setOkActionEnabled(false) - } else if (ZigToolchainListService + } else { + val existingToolchain = ZigToolchainListService .getInstance() .toolchains .mapNotNull { it.second as? LocalZigToolchain } - .any { it.location == tc.location } - ) { - errorMessageBox.icon = AllIcons.General.Warning - errorMessageBox.text = tc.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } - ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") - dialog.setOkActionEnabled(true) - } else { - errorMessageBox.icon = AllIcons.General.Information - errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") - dialog.setOkActionEnabled(true) + .firstOrNull { it.location == tc.location } + if (existingToolchain != null) { + errorMessageBox.icon = AllIcons.General.Warning + errorMessageBox.text = existingToolchain.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } + ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") + dialog.setOkActionEnabled(true) + } else { + errorMessageBox.icon = AllIcons.General.Information + errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") + dialog.setOkActionEnabled(true) + } + } + if (tc != null) { + tc = ZigToolchainListService.getInstance().withUniqueName(tc) } val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() name.emptyText.text = tc?.name ?: "" @@ -89,9 +103,20 @@ object LocalSelector { name.text = name.emptyText.text } } + suspend fun verify(path: String) { + val tc = runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) { + LocalZigToolchain.tryFromPathString(path) + } }.getOrNull() + verify(tc) + } + val active = AtomicBoolean(false) path.addDocumentListener(object: DocumentAdapter() { override fun textChanged(e: DocumentEvent) { - verify(path.text) + if (!active.get()) + return + zigCoroutineScope.launchWithEDT(ModalityState.current()) { + verify(path.text) + } } }) val center = panel { @@ -110,19 +135,29 @@ object LocalSelector { dialog.setTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.title")) dialog.addCancelAction() dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.toolchain.local-selector.ok-action")) } - val chosenFile = FileChooser.chooseFile( - FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")), - null, - null - ) - if (chosenFile != null) { - verify(chosenFile.path) - path.text = chosenFile.path + if (preSelected == null) { + val chosenFile = FileChooser.chooseFile( + FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")), + null, + null + ) + if (chosenFile != null) { + verify(chosenFile.path) + path.text = chosenFile.path + } + } else { + verify(preSelected) + path.text = preSelected.location.pathString } + active.set(true) if (!dialog.showAndGet()) { + active.set(false) return null } - return LocalZigToolchain.tryFromPathString(path.text)?.second?.also { it.copy(name = name.text.ifBlank { null } ?: it.name) } + active.set(false) + return runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) { + LocalZigToolchain.tryFromPathString(path.text)?.let { it.withName(name.text.ifBlank { null } ?: it.name) } + } }.getOrNull() } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index 66a2a9a1..b2fc795a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -22,7 +22,6 @@ package com.falsepattern.zigbrains.project.toolchain.local -import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine @@ -32,8 +31,9 @@ import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull -import com.intellij.util.text.SemVer +import kotlinx.coroutines.delay import java.nio.file.Path +import kotlin.random.Random @JvmRecord data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain { @@ -54,6 +54,10 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override return location.resolve(exeName) } + override fun withName(newName: String?): LocalZigToolchain { + return this.copy(name = newName) + } + companion object { val DIRENV_KEY = KeyWithDefaultValue.create("ZIG_LOCAL_DIRENV") @@ -67,27 +71,23 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override } } - fun tryFromPathString(pathStr: String?): Pair? { - return pathStr?.ifBlank { null }?.toNioPathOrNull()?.let(::tryFromPath) + suspend fun tryFromPathString(pathStr: String?): LocalZigToolchain? { + return pathStr?.ifBlank { null }?.toNioPathOrNull()?.let { tryFromPath(it) } } - fun tryFromPath(path: Path): Pair? { + suspend fun tryFromPath(path: Path): LocalZigToolchain? { var tc = LocalZigToolchain(path) if (!tc.zig.fileValid()) { return null } val versionStr = tc.zig - .getEnvBlocking(null) + .getEnv(null) .getOrNull() ?.version - val version: SemVer? if (versionStr != null) { - version = SemVer.parseFromText(versionStr) tc = tc.copy(name = "Zig $versionStr") - } else { - version = null } - return version to tc + return tc } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index 4b12e449..b82094e9 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -22,13 +22,12 @@ package com.falsepattern.zigbrains.project.toolchain.local -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.direnv.emptyEnv +import com.falsepattern.zigbrains.direnv.Env import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider +import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.toNioPathOrNull @@ -37,7 +36,8 @@ import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes import com.intellij.util.EnvironmentUtil import com.intellij.util.system.OS -import com.intellij.util.text.SemVer +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async import java.io.File import java.nio.file.Files import java.nio.file.Path @@ -53,7 +53,7 @@ class LocalZigToolchainProvider: ZigToolchainProvider { // } else { // emptyEnv // } - val env = emptyEnv + val env = Env.empty val zigExePath = env.findExecutableOnPATH("zig") ?: return null return LocalZigToolchain(zigExePath.parent) } @@ -98,7 +98,7 @@ class LocalZigToolchainProvider: ZigToolchainProvider { return LocalZigToolchainConfigurable(uuid, toolchain) } - override fun suggestToolchains(): List> { + override fun suggestToolchains(): List> { val res = HashSet() EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } val wellKnown = getWellKnown() @@ -113,7 +113,7 @@ class LocalZigToolchainProvider: ZigToolchainProvider { } } } - return res.mapNotNull { LocalZigToolchain.tryFromPathString(it) } + return res.map { zigCoroutineScope.async { LocalZigToolchain.tryFromPathString(it) ?: throw IllegalArgumentException() } } } override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt index 82bcc4d7..6369ff53 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigCompilerTool.kt @@ -45,8 +45,6 @@ class ZigCompilerTool(toolchain: ZigToolchain) : ZigTool(toolchain) { Result.failure(IllegalStateException("could not deserialize zig env", e)) } } - - fun getEnvBlocking(project: Project?) = runBlocking { getEnv(project) } } private val envJson = Json { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt index 9354cf5a..3febf6bd 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt @@ -32,7 +32,7 @@ import java.util.UUID internal object ZigToolchainComboBoxHandler { @RequiresBackgroundThread suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) { - is TCListElem.Toolchain.Suggested -> elem.toolchain + is TCListElem.Toolchain.Suggested -> ZigToolchainListService.getInstance().withUniqueName(elem.toolchain) is TCListElem.Download -> Downloader.downloadToolchain(context) is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context) }?.let { ZigToolchainListService.getInstance().registerNewToolchain(it) } 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 f21fab92..3fff4a24 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 @@ -184,10 +184,10 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC private fun getModelList(): List { val modelList = ArrayList() modelList.add(TCListElem.None) - modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }) + modelList.addAll(ZigToolchainListService.getInstance().toolchains.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.addAll(suggestZigToolchains().map { it.asSuggested() }) + modelList.addAll(suggestZigToolchains().map { it.asPending() }) return modelList } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index b2db35e1..d6a04045 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -34,6 +34,7 @@ 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 @@ -65,10 +66,13 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList val modelList = ArrayList() modelList.addAll(TCListElem.fetchGroup) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) - modelList.addAll(suggestZigToolchains().map { it.asSuggested() }) - val model = TCModel.Companion(modelList) + modelList.addAll(suggestZigToolchains().map { it.asPending() }) + val model = TCModel(modelList) val context = TCContext(null, model) val popup = TCComboBoxPopup(context, null, ::onItemSelected) + model.whenListChanged { + popup.syncWithModelChange() + } popup.showInBestPositionFor(e.dataContext) } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt index 1915f91b..56de26a6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt @@ -23,6 +23,10 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import java.util.UUID @@ -41,6 +45,7 @@ internal sealed interface TCListElem : TCListElemIn { object None: TCListElem object Download : TCListElem, Pseudo object FromDisk : TCListElem, Pseudo + data class Pending(val elem: Deferred): TCListElem companion object { val fetchGroup get() = listOf(Download, FromDisk) @@ -52,4 +57,6 @@ internal data class Separator(val text: String, val line: Boolean) : TCListElemI internal fun Pair.asActual() = TCListElem.Toolchain.Actual(first, second) -internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this) \ No newline at end of file +internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this) + +internal fun Deferred.asPending() = TCListElem.Pending(zigCoroutineScope.async { this@asPending.await().asSuggested() }) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index 0671536b..29db7005 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -26,26 +26,42 @@ import ai.grazie.utils.attributes.value import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.toolchain.base.render +import com.falsepattern.zigbrains.shared.zigCoroutineScope 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.impl.ModalityStateEx +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.AnimatedIcon import com.intellij.ui.CellRendererPanel +import com.intellij.ui.ClientProperty import com.intellij.ui.CollectionComboBoxModel import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.ComponentUtil 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 fleet.util.async.awaitResult +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.concurrent.locks.ReentrantLock import javax.accessibility.AccessibleContext +import javax.swing.CellRendererPane +import javax.swing.JComponent import javax.swing.JList import javax.swing.border.Border @@ -57,7 +73,7 @@ internal class TCComboBoxPopup( internal class TCComboBox(model: TCModel): ComboBox(model) { init { - setRenderer(TCCellRenderer({model})) + setRenderer(TCCellRenderer { model }) } var selectedToolchain: UUID? @@ -86,15 +102,17 @@ internal class TCComboBox(model: TCModel): ComboBox(model) { } } -internal class TCModel private constructor(elements: List, private var separators: Map) : CollectionComboBoxModel(elements) { +internal class TCModel private constructor(elements: List, private var separators: MutableMap) : CollectionComboBoxModel(elements) { + private var counter: Int = 0 companion object { operator fun invoke(input: List): TCModel { val (elements, separators) = convert(input) val model = TCModel(elements, separators) + model.launchPendingResolve() return model } - private fun convert(input: List): Pair, Map> { + private fun convert(input: List): Pair, MutableMap> { val separators = IdentityHashMap() var lastSeparator: Separator? = null val elements = ArrayList() @@ -117,10 +135,52 @@ internal class TCModel private constructor(elements: List, private v fun separatorAbove(elem: TCListElem) = separators[elem] + private fun launchPendingResolve() { + runInEdt(ModalityState.any()) { + val counter = this.counter + val size = this.size + for (i in 0.. index) { + this@TCModel.getElementAt(index)?.let { separators[it] = sep } + } + return + } + val currentIndex = this@TCModel.getElementIndex(old) + separators.remove(old)?.let { + separators.put(new, it) + } + this@TCModel.setElementAt(new, currentIndex) + } + + @RequiresEdt fun updateContents(input: List) { + counter++ val (elements, separators) = convert(input) this.separators = separators replaceAll(elements) + launchPendingResolve() } } @@ -216,7 +276,10 @@ internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRend icon = AllIcons.General.OpenDisk append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text")) } - + is TCListElem.Pending -> { + icon = AllIcons.Empty + append("Loading\u2026", SimpleTextAttributes.GRAYED_ATTRIBUTES) + } is TCListElem.None, null -> { icon = AllIcons.General.BalloonError append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt index f826bf3e..680cc1ef 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.shared.ipc -import com.falsepattern.zigbrains.direnv.emptyEnv +import com.falsepattern.zigbrains.direnv.Env import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.FileUtil @@ -56,7 +56,7 @@ object IPCUtil { if (SystemInfo.isWindows) { return null } - val mkfifo = emptyEnv + val mkfifo = Env.empty .findAllExecutablesOnPATH("mkfifo") .map { it.pathString } .map(::MKFifo) @@ -67,7 +67,7 @@ object IPCUtil { true } ?: return null - val selectedBash = emptyEnv + val selectedBash = Env.empty .findAllExecutablesOnPATH("bash") .map { it.pathString } .filter { From f7ea73ae4533036266ef5c11b209d5f483608280 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 04:14:31 +0200 Subject: [PATCH 15/22] tristate direnv config --- .../com/falsepattern/zigbrains/ZBStartup.kt | 4 - .../zigbrains/direnv/DirenvService.kt | 75 +++++++++++++-- .../zigbrains/direnv/ui/DirenvEditor.kt | 95 +++++++++++++++++++ .../project/execution/base/ZigExecConfig.kt | 10 +- .../project/execution/base/ZigProfileState.kt | 2 + .../zigbrains/project/run/ZigRegularRunner.kt | 5 +- .../project/settings/ZigConfigurable.kt | 1 + .../ZigProjectConfigurationProvider.kt | 43 ++++++++- .../toolchain/base/ZigToolchainProvider.kt | 39 ++++---- .../toolchain/local/LocalZigToolchain.kt | 13 +-- .../local/LocalZigToolchainProvider.kt | 66 ++++++------- .../toolchain/ui/ZigToolchainEditor.kt | 38 ++++++-- .../toolchain/ui/ZigToolchainListEditor.kt | 2 +- .../project/toolchain/ui/elements.kt | 8 +- .../zigbrains/project/toolchain/ui/model.kt | 39 ++++---- .../resources/META-INF/zigbrains-core.xml | 3 + .../resources/zigbrains/Bundle.properties | 1 + 17 files changed, 326 insertions(+), 118 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt index 88e78369..973e7b01 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/ZBStartup.kt @@ -22,8 +22,6 @@ package com.falsepattern.zigbrains -import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain import com.intellij.ide.BrowserUtil import com.intellij.ide.plugins.PluginManager import com.intellij.notification.Notification @@ -35,10 +33,8 @@ import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity -import com.intellij.openapi.util.UserDataHolderBase import java.lang.reflect.Constructor import java.lang.reflect.Method -import kotlin.io.path.pathString class ZBStartup: ProjectActivity { var firstInit = true 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 282b3783..3d2f1881 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt @@ -29,24 +29,47 @@ import com.intellij.ide.impl.isTrusted import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.notification.Notifications -import com.intellij.openapi.components.Service -import com.intellij.openapi.components.service +import com.intellij.openapi.components.* import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.util.Key +import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.platform.util.progress.withProgressText import com.intellij.util.io.awaitExit +import com.intellij.util.xmlb.annotations.Attribute import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.nio.file.Path +import kotlin.io.path.isRegularFile @Service(Service.Level.PROJECT) -class DirenvService(val project: Project) { - val mutex = Mutex() +@State( + name = "Direnv", + storages = [Storage("zigbrains.xml")] +) +class DirenvService(val project: Project): SerializablePersistentStateComponent(State()), IDirenvService { + private val mutex = Mutex() - suspend fun import(): Env { + override val isInstalled: Boolean by lazy { + // Using the builtin stuff here instead of Env because it should only scan for direnv on the process path + PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("direnv") != null + } + + var isEnabledRaw: DirenvState + get() = state.enabled + set(value) { + updateState { + it.copy(enabled = value) + } + } + + override val isEnabled: DirenvState + get() = isEnabledRaw + + override suspend fun import(): Env { if (!isInstalled || !project.isTrusted() || project.isDefault) return Env.empty val workDir = project.guessProjectDir()?.toNioPath() ?: return Env.empty @@ -100,13 +123,45 @@ class DirenvService(val project: Project) { return DirenvOutput(stdOut, false) } - val isInstalled: Boolean by lazy { - // Using the builtin stuff here instead of Env because it should only scan for direnv on the process path - PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("direnv") != null + fun hasDotEnv(): Boolean { + if (!isInstalled) + return false + val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return false + return envFiles.any { projectDir.resolve(it).isRegularFile() } } + data class State( + @JvmField + @Attribute + var enabled: DirenvState = DirenvState.Auto + ) + companion object { private const val GROUP_DISPLAY_ID = "zigbrains-direnv" - fun getInstance(project: Project): DirenvService = project.service() + fun getInstance(project: Project): IDirenvService = project.service() + + val STATE_KEY = Key.create("DIRENV_STATE") } -} \ No newline at end of file +} + +enum class DirenvState { + Auto, + Enabled, + Disabled; + + fun isEnabled(project: Project?): Boolean { + return when(this) { + Enabled -> true + Disabled -> false + Auto -> project?.service()?.hasDotEnv() == true + } + } +} + +sealed interface IDirenvService { + val isInstalled: Boolean + val isEnabled: DirenvState + suspend fun import(): Env +} + +private val envFiles = listOf(".envrc", ".env") \ No newline at end of file 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 new file mode 100644 index 00000000..ff189ab8 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt @@ -0,0 +1,95 @@ +/* + * 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.direnv.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.direnv.DirenvService +import com.falsepattern.zigbrains.direnv.DirenvState +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.shared.SubConfigurable +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.dsl.builder.Panel +import java.awt.event.ItemEvent + +abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): SubConfigurable { + private var cb: ComboBox? = null + override fun attach(panel: Panel): Unit = with(panel) { + row(ZigBrainsBundle.message("settings.direnv.enable.label")) { + comboBox(DirenvState.entries).component.let { + cb = it + it.addItemListener { e -> + if (e.stateChange != ItemEvent.SELECTED) + return@addItemListener + sharedState + } + } + } + } + + override fun isModified(context: T): Boolean { + return isEnabled(context) != cb?.selectedItem as DirenvState + } + + override fun apply(context: T) { + setEnabled(context, cb?.selectedItem as DirenvState) + } + + override fun reset(context: T?) { + if (context == null) { + cb?.selectedItem = DirenvState.Auto + return + } + cb?.selectedItem = isEnabled(context) + } + + override fun dispose() { + } + + abstract fun isEnabled(context: T): DirenvState + abstract fun setEnabled(context: T, value: DirenvState) + + class ForProject(sharedState: ZigProjectConfigurationProvider.IUserDataBridge) : DirenvEditor(sharedState) { + override fun isEnabled(context: Project): DirenvState { + return context.service().isEnabledRaw + } + + override fun setEnabled(context: Project, value: DirenvState) { + context.service().isEnabledRaw = value + } + } + + class Provider: ZigProjectConfigurationProvider { + override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { + if (project?.isDefault != false) { + return null + } + sharedState.putUserData(DirenvService.STATE_KEY, DirenvState.Auto) + return ForProject(sharedState) + } + + override val index: Int + get() = 1 + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt index cf01992c..c28926cf 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.execution.base +import com.falsepattern.zigbrains.direnv.DirenvService import com.intellij.execution.ExecutionException import com.intellij.execution.Executor import com.intellij.execution.configurations.ConfigurationFactory @@ -34,6 +35,7 @@ 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 @@ -62,10 +64,10 @@ abstract class ZigExecConfig>(project: Project, factory: Con suspend fun patchCommandLine(commandLine: GeneralCommandLine): GeneralCommandLine { -// TODO direnv -// if (project.zigProjectSettings.state.direnv) { -// commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) -// } + val direnv = DirenvService.getInstance(project) + if (direnv.isEnabled.isEnabled(project)) { + commandLine.withEnvironment(direnv.import().env) + } return commandLine } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt index 4e13a161..73b28156 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt @@ -35,6 +35,8 @@ import com.intellij.execution.configurations.PtyCommandLine import com.intellij.execution.process.ProcessHandler import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.platform.ide.progress.ModalTaskOwner +import com.intellij.platform.util.progress.reportProgress +import com.intellij.platform.util.progress.reportRawProgress import kotlin.io.path.pathString abstract class ZigProfileState> ( diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt index 1860ab1f..2601ad9b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/run/ZigRegularRunner.kt @@ -32,10 +32,13 @@ import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.RunContentBuilder import com.intellij.execution.ui.RunContentDescriptor import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.progress.blockingContext class ZigRegularRunner: ZigProgramRunner>(DefaultRunExecutor.EXECUTOR_ID) { override suspend fun execute(state: ZigProfileState<*>, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? { - val exec = state.execute(environment.executor, this) + val exec = blockingContext { + state.execute(environment.executor, this) + } return withEDTContext(ModalityState.any()) { val runContentBuilder = RunContentBuilder(exec, environment) runContentBuilder.showRunContent(null) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt index ba957bc4..6ff153e6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.shared.SubConfigurable import com.intellij.openapi.project.Project import com.intellij.openapi.util.NlsContexts +import com.intellij.openapi.util.UserDataHolderBase class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter() { override fun instantiate(): List> { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt index 29dbc1e3..5a2b6698 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt @@ -25,14 +25,51 @@ package com.falsepattern.zigbrains.project.settings import com.falsepattern.zigbrains.shared.SubConfigurable import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.util.UserDataHolderBase interface ZigProjectConfigurationProvider { - fun create(project: Project?): SubConfigurable? + fun create(project: Project?, sharedState: IUserDataBridge): SubConfigurable? val index: Int companion object { private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.projectConfigProvider") fun createPanels(project: Project?): List> { - return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(project) } + val sharedState = UserDataBridge() + return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(project, sharedState) } } } -} \ No newline at end of file + + interface IUserDataBridge: UserDataHolder { + fun addUserDataChangeListener(listener: UserDataListener) + fun removeUserDataChangeListener(listener: UserDataListener) + } + + interface UserDataListener { + fun onUserDataChanged(key: Key<*>) + } + + class UserDataBridge: UserDataHolderBase(), IUserDataBridge { + private val listeners = ArrayList() + override fun putUserData(key: Key, value: T?) { + super.putUserData(key, value) + synchronized(listeners) { + listeners.forEach { listener -> + listener.onUserDataChanged(key) + } + } + } + + override fun addUserDataChangeListener(listener: UserDataListener) { + synchronized(listeners) { + listeners.add(listener) + } + } + + override fun removeUserDataChangeListener(listener: UserDataListener) { + synchronized(listeners) { + listeners.remove(listener) + } + } + } +} 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 e06f6f8d..7ecada79 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.direnv.DirenvState import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.extensions.ExtensionPointName @@ -30,21 +31,29 @@ import com.intellij.openapi.util.UserDataHolder import com.intellij.ui.SimpleColoredComponent import com.intellij.util.text.SemVer import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMap +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import java.util.UUID private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") internal interface ZigToolchainProvider { - suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): ZigToolchain? - val serialMarker: String fun isCompatible(toolchain: ZigToolchain): Boolean fun deserialize(data: Map): ZigToolchain? fun serialize(toolchain: ZigToolchain): Map fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> - fun suggestToolchains(): List> + suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) } @@ -60,31 +69,21 @@ fun ZigToolchain.toRef(): ZigToolchain.Ref { return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this)) } -suspend fun Project?.suggestZigToolchain(extraData: UserDataHolder): ZigToolchain? { - return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(this, extraData) } -} - fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*> { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() return provider.createConfigurable(uuid, this) } -fun suggestZigToolchains(): List> { +@OptIn(ExperimentalCoroutinesApi::class) +fun suggestZigToolchains(project: Project? = null, direnv: DirenvState = DirenvState.Disabled): Flow { val existing = ZigToolchainListService.getInstance().toolchains.map { (_, tc) -> tc }.toList() - return EXTENSION_POINT_NAME.extensionList.flatMap { ext -> + return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } - val suggestions = ext.suggestToolchains() - suggestions.map { suggestion -> - zigCoroutineScope.async { - val sugg = suggestion.await() - if (compatibleExisting.none { existing -> ext.matchesSuggestion(existing, sugg) }) { - sugg - } else { - throw IllegalArgumentException() - } - } + val suggestions = ext.suggestToolchains(project, direnv) + suggestions.filter { suggestion -> + compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } } - } + }.flowOn(Dispatchers.IO) } fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index b2fc795a..1f529990 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -22,18 +22,16 @@ package com.falsepattern.zigbrains.project.toolchain.local +import com.falsepattern.zigbrains.direnv.DirenvService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir -import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull -import kotlinx.coroutines.delay import java.nio.file.Path -import kotlin.random.Random @JvmRecord data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain { @@ -42,10 +40,9 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override } override suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project?): GeneralCommandLine { - //TODO direnv -// if (project != null && (commandLine.getUserData(DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { -// commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) -// } + if (project != null && (commandLine.getUserData(DirenvService.STATE_KEY) ?: DirenvService.getInstance(project).isEnabled).isEnabled(project)) { + commandLine.withEnvironment(DirenvService.getInstance(project).import().env) + } return commandLine } @@ -59,8 +56,6 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override } companion object { - val DIRENV_KEY = KeyWithDefaultValue.create("ZIG_LOCAL_DIRENV") - @Throws(ExecutionException::class) fun ensureLocal(toolchain: ZigToolchain): LocalZigToolchain { if (toolchain is LocalZigToolchain) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index b82094e9..d18c0e82 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -22,6 +22,8 @@ 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 @@ -36,8 +38,16 @@ import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes import com.intellij.util.EnvironmentUtil import com.intellij.util.system.OS -import kotlinx.coroutines.Deferred +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flattenConcat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import java.io.File import java.nio.file.Files import java.nio.file.Path @@ -46,18 +56,6 @@ import kotlin.io.path.isDirectory import kotlin.io.path.pathString class LocalZigToolchainProvider: ZigToolchainProvider { - override suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): LocalZigToolchain? { - //TODO direnv -// val env = if (project != null && (extraData.getUserData(LocalZigToolchain.DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { -// DirenvCmd.importDirenv(project) -// } else { -// emptyEnv -// } - val env = Env.empty - val zigExePath = env.findExecutableOnPATH("zig") ?: return null - return LocalZigToolchain(zigExePath.parent) - } - override val serialMarker: String get() = "local" @@ -98,22 +96,25 @@ class LocalZigToolchainProvider: ZigToolchainProvider { return LocalZigToolchainConfigurable(uuid, toolchain) } - override fun suggestToolchains(): List> { - val res = HashSet() - EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } - val wellKnown = getWellKnown() - wellKnown.forEach { dir -> + @OptIn(ExperimentalCoroutinesApi::class) + override suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow { + val env = if (project != null && direnv.isEnabled(project)) { + DirenvService.getInstance(project).import() + } else { + Env.empty + } + val pathToolchains = env.findAllExecutablesOnPATH("zig").mapNotNull { it.parent } + val wellKnown = getWellKnown().asFlow().flatMapConcat { dir -> if (!dir.isDirectory()) - return@forEach + return@flatMapConcat emptyFlow() runCatching { Files.newDirectoryStream(dir).use { stream -> - stream.forEach { subDir -> - res.add(subDir.pathString) - } + stream.toList().filterNotNull().asFlow() } - } + }.getOrElse { emptyFlow() } } - return res.map { zigCoroutineScope.async { LocalZigToolchain.tryFromPathString(it) ?: throw IllegalArgumentException() } } + val joined = flowOf(pathToolchains, wellKnown).flattenConcat() + return joined.mapNotNull { LocalZigToolchain.tryFromPath(it) } } override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { @@ -121,21 +122,20 @@ class LocalZigToolchainProvider: ZigToolchainProvider { val name = toolchain.name val path = presentDetectedPath(toolchain.location.pathString) val primary: String - val secondary: String? + var secondary: String? val tooltip: String? if (isSuggestion) { primary = path secondary = name - tooltip = null } else { primary = name ?: "Zig" - if (isSelected) { - secondary = null - tooltip = path - } else { - secondary = path - tooltip = null - } + secondary = path + } + if (isSelected) { + tooltip = secondary + secondary = null + } else { + tooltip = null } component.append(primary) if (secondary != null) { 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 3fff4a24..09433f57 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -23,6 +23,8 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.direnv.DirenvService +import com.falsepattern.zigbrains.direnv.DirenvState import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService @@ -35,10 +37,12 @@ import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.withEDTContext 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.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel import com.intellij.util.concurrency.annotations.RequiresEdt @@ -50,16 +54,24 @@ import java.util.UUID import javax.swing.JButton import kotlin.collections.addAll -class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubConfigurable, ToolchainListChangeListener { +class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable, ToolchainListChangeListener, ZigProjectConfigurationProvider.UserDataListener { private val toolchainBox: TCComboBox private var selectOnNextReload: UUID? = null private val model: TCModel private var editButton: JButton? = null init { - model = TCModel(getModelList()) + val direnv = sharedState.getUserData(DirenvService.STATE_KEY) ?: project?.let { DirenvService.getInstance(it).isEnabled } ?: DirenvState.Disabled + model = TCModel(getModelList(project, direnv)) toolchainBox = TCComboBox(model) toolchainBox.addItemListener(::itemStateChanged) ZigToolchainListService.getInstance().addChangeListener(this) + sharedState.addUserDataChangeListener(this) + model.whenListChanged { + if (toolchainBox.isPopupVisible) { + toolchainBox.isPopupVisible = false + toolchainBox.isPopupVisible = true + } + } } private fun refreshButtonState(item: Any?) { @@ -85,7 +97,8 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC override suspend fun toolchainListChanged() { withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { - val list = getModelList() + val direnv = sharedState.getUserData(DirenvService.STATE_KEY) ?: project?.let { DirenvService.getInstance(it).isEnabled } ?: DirenvState.Disabled + val list = getModelList(project, direnv) model.updateContents(list) val onReload = selectOnNextReload selectOnNextReload = null @@ -115,9 +128,16 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC } } + override fun onUserDataChanged(key: Key<*>) { + if (key == DirenvService.STATE_KEY) { + zigCoroutineScope.launch { toolchainListChanged() } + } + } + + override fun attach(p: Panel): Unit = with(p) { row(ZigBrainsBundle.message( - if (isForDefaultProject) + if (project?.isDefault == true) "settings.toolchain.editor.toolchain-default.label" else "settings.toolchain.editor.toolchain.label") @@ -169,25 +189,23 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC } override val newProjectBeforeInitSelector get() = true - class Provider: ZigProjectConfigurationProvider { - override fun create(project: Project?): SubConfigurable? { - return ZigToolchainEditor(project?.isDefault ?: false).also { it.reset(project) } + override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { + return ZigToolchainEditor(project, sharedState).also { it.reset(project) } } override val index: Int get() = 0 - } } -private fun getModelList(): List { +private fun getModelList(project: Project?, direnv: DirenvState): List { val modelList = ArrayList() modelList.add(TCListElem.None) modelList.addAll(ZigToolchainListService.getInstance().toolchains.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.addAll(suggestZigToolchains().map { it.asPending() }) + modelList.add(suggestZigToolchains(project, direnv).asPending()) return modelList } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index d6a04045..e5fc8e71 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -66,7 +66,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList val modelList = ArrayList() modelList.addAll(TCListElem.fetchGroup) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) - modelList.addAll(suggestZigToolchains().map { it.asPending() }) + modelList.add(suggestZigToolchains().asPending()) val model = TCModel(modelList) val context = TCContext(null, model) val popup = TCComboBoxPopup(context, null, ::onItemSelected) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt index 56de26a6..ab014123 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt @@ -24,9 +24,9 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.zigCoroutineScope -import kotlinx.coroutines.Deferred import kotlinx.coroutines.async -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import java.util.UUID @@ -45,7 +45,7 @@ internal sealed interface TCListElem : TCListElemIn { object None: TCListElem object Download : TCListElem, Pseudo object FromDisk : TCListElem, Pseudo - data class Pending(val elem: Deferred): TCListElem + data class Pending(val elems: Flow): TCListElem companion object { val fetchGroup get() = listOf(Download, FromDisk) @@ -59,4 +59,4 @@ internal fun Pair.asActual() = TCListElem.Toolchain.Actual(f internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this) -internal fun Deferred.asPending() = TCListElem.Pending(zigCoroutineScope.async { this@asPending.await().asSuggested() }) \ No newline at end of file +internal fun Flow.asPending() = TCListElem.Pending(map { it.asSuggested() }) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index 29db7005..2f490d13 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -22,7 +22,6 @@ package com.falsepattern.zigbrains.project.toolchain.ui -import ai.grazie.utils.attributes.value import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.toolchain.base.render @@ -31,16 +30,12 @@ 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.impl.ModalityStateEx import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.Project import com.intellij.openapi.ui.ComboBox -import com.intellij.ui.AnimatedIcon import com.intellij.ui.CellRendererPanel -import com.intellij.ui.ClientProperty import com.intellij.ui.CollectionComboBoxModel import com.intellij.ui.ColoredListCellRenderer -import com.intellij.ui.ComponentUtil import com.intellij.ui.GroupHeaderSeparator import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes @@ -51,17 +46,13 @@ 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 fleet.util.async.awaitResult 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.concurrent.locks.ReentrantLock import javax.accessibility.AccessibleContext -import javax.swing.CellRendererPane -import javax.swing.JComponent import javax.swing.JList import javax.swing.border.Border @@ -145,33 +136,43 @@ internal class TCModel private constructor(elements: List, private v if (elem !is TCListElem.Pending) continue zigCoroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) { - val newElem = elem.elem.awaitResult().getOrNull() - swap(elem, newElem, counter) + elem.elems.collect { newElem -> + insertBefore(elem, newElem, counter) + } + remove(elem, counter) } } } } @RequiresEdt - private fun swap(old: TCListElem, new: TCListElem?, oldCounter: Int) { + 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) { - 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 } - } return } val currentIndex = this@TCModel.getElementIndex(old) separators.remove(old)?.let { separators.put(new, it) } - this@TCModel.setElementAt(new, currentIndex) + this@TCModel.add(currentIndex, new) } @RequiresEdt diff --git a/core/src/main/resources/META-INF/zigbrains-core.xml b/core/src/main/resources/META-INF/zigbrains-core.xml index 8fca23cc..b3eb4bc2 100644 --- a/core/src/main/resources/META-INF/zigbrains-core.xml +++ b/core/src/main/resources/META-INF/zigbrains-core.xml @@ -190,6 +190,9 @@ + diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index d3fe1d93..d81ebb97 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -152,3 +152,4 @@ settings.toolchain.local-selector.state.invalid=Invalid toolchain path settings.toolchain.local-selector.state.already-exists-unnamed=Toolchain already exists settings.toolchain.local-selector.state.already-exists-named=Toolchain already exists as "{0}" settings.toolchain.local-selector.state.ok=Toolchain path OK +settings.direnv.enable.label=Direnv From 68b60e2c77021cd461806cee9cfa92f6b5eb3099 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 11:27:22 +0200 Subject: [PATCH 16/22] direnv decoupling --- .../zigbrains/direnv/DirenvService.kt | 19 ++++----- .../zigbrains/direnv/DirenvState.kt | 40 +++++++++++++++++++ .../zigbrains/direnv/ui/DirenvEditor.kt | 14 ++++--- .../toolchain/base/ZigToolchainProvider.kt | 25 +++++++----- .../toolchain/local/LocalZigToolchain.kt | 2 +- .../local/LocalZigToolchainProvider.kt | 11 +---- .../toolchain/ui/ZigToolchainEditor.kt | 15 +++---- 7 files changed, 79 insertions(+), 47 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvState.kt 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 3d2f1881..2e8d0807 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvService.kt @@ -33,6 +33,7 @@ import com.intellij.openapi.components.* import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.util.Key +import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.platform.util.progress.withProgressText import com.intellij.util.io.awaitExit @@ -140,20 +141,14 @@ class DirenvService(val project: Project): SerializablePersistentStateComponent< private const val GROUP_DISPLAY_ID = "zigbrains-direnv" fun getInstance(project: Project): IDirenvService = project.service() - val STATE_KEY = Key.create("DIRENV_STATE") - } -} + private val STATE_KEY = Key.create("DIRENV_STATE") -enum class DirenvState { - Auto, - Enabled, - Disabled; + fun getStateFor(data: UserDataHolder, project: Project?): DirenvState { + return data.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled + } - fun isEnabled(project: Project?): Boolean { - return when(this) { - Enabled -> true - Disabled -> false - Auto -> project?.service()?.hasDotEnv() == true + fun setStateFor(data: UserDataHolder, state: DirenvState) { + data.putUserData(STATE_KEY, state) } } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvState.kt new file mode 100644 index 00000000..f4c96608 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/DirenvState.kt @@ -0,0 +1,40 @@ +/* + * 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.direnv + +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project + +enum class DirenvState { + Auto, + Enabled, + Disabled; + + fun isEnabled(project: Project?): Boolean { + return when(this) { + Enabled -> true + Disabled -> false + Auto -> project?.service()?.hasDotEnv() == true + } + } +} \ No newline at end of file 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 ff189ab8..e0231243 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 @@ -39,10 +39,12 @@ abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationP row(ZigBrainsBundle.message("settings.direnv.enable.label")) { comboBox(DirenvState.entries).component.let { cb = it - it.addItemListener { e -> - if (e.stateChange != ItemEvent.SELECTED) - return@addItemListener - sharedState + if (sharedState != null) { + it.addItemListener { e -> + if (e.stateChange != ItemEvent.SELECTED) + return@addItemListener + DirenvService.setStateFor(sharedState, DirenvState.Auto) + } } } } @@ -72,7 +74,7 @@ abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationP class ForProject(sharedState: ZigProjectConfigurationProvider.IUserDataBridge) : DirenvEditor(sharedState) { override fun isEnabled(context: Project): DirenvState { - return context.service().isEnabledRaw + return DirenvService.getInstance(context).isEnabled } override fun setEnabled(context: Project, value: DirenvState) { @@ -85,7 +87,7 @@ abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationP if (project?.isDefault != false) { return null } - sharedState.putUserData(DirenvService.STATE_KEY, DirenvState.Auto) + DirenvService.setStateFor(sharedState, DirenvState.Auto) return ForProject(sharedState) } 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 7ecada79..c18a3923 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 @@ -24,24 +24,18 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.direnv.DirenvState import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService -import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key import com.intellij.openapi.util.UserDataHolder import com.intellij.ui.SimpleColoredComponent -import com.intellij.util.text.SemVer -import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flatMap import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import java.util.UUID private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") @@ -53,7 +47,7 @@ internal interface ZigToolchainProvider { fun serialize(toolchain: ZigToolchain): Map fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> - suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow + suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) } @@ -75,11 +69,11 @@ fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<* } @OptIn(ExperimentalCoroutinesApi::class) -fun suggestZigToolchains(project: Project? = null, direnv: DirenvState = DirenvState.Disabled): Flow { +fun suggestZigToolchains(project: Project? = null, data: UserDataHolder = emptyData): Flow { val existing = ZigToolchainListService.getInstance().toolchains.map { (_, tc) -> tc }.toList() return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } - val suggestions = ext.suggestToolchains(project, direnv) + val suggestions = ext.suggestToolchains(project, data) suggestions.filter { suggestion -> compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } } @@ -89,4 +83,15 @@ fun suggestZigToolchains(project: Project? = null, direnv: DirenvState = DirenvS fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() return provider.render(this, component, isSuggestion, isSelected) +} + +private val emptyData = object: UserDataHolder { + override fun getUserData(key: Key): T? { + return null + } + + override fun putUserData(key: Key, value: T?) { + + } + } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index 1f529990..f5dc9045 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -40,7 +40,7 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override } override suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project?): GeneralCommandLine { - if (project != null && (commandLine.getUserData(DirenvService.STATE_KEY) ?: DirenvService.getInstance(project).isEnabled).isEnabled(project)) { + if (project != null && DirenvService.getStateFor(commandLine, project).isEnabled(project)) { commandLine.withEnvironment(DirenvService.getInstance(project).import().env) } return commandLine diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index d18c0e82..49b223d7 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -28,7 +28,6 @@ import com.falsepattern.zigbrains.direnv.Env import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider -import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.project.Project import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.io.FileUtil @@ -36,19 +35,15 @@ import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.text.StringUtil import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes -import com.intellij.util.EnvironmentUtil import com.intellij.util.system.OS import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flattenConcat import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull -import java.io.File import java.nio.file.Files import java.nio.file.Path import java.util.UUID @@ -97,16 +92,14 @@ class LocalZigToolchainProvider: ZigToolchainProvider { } @OptIn(ExperimentalCoroutinesApi::class) - override suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow { - val env = if (project != null && direnv.isEnabled(project)) { + override suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow { + val env = if (project != null && DirenvService.getStateFor(data, project).isEnabled(project)) { DirenvService.getInstance(project).import() } else { Env.empty } val pathToolchains = env.findAllExecutablesOnPATH("zig").mapNotNull { it.parent } val wellKnown = getWellKnown().asFlow().flatMapConcat { dir -> - if (!dir.isDirectory()) - return@flatMapConcat emptyFlow() runCatching { Files.newDirectoryStream(dir).use { stream -> stream.toList().filterNotNull().asFlow() 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 09433f57..c30d8295 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 @@ -43,6 +43,7 @@ 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 @@ -60,8 +61,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: private val model: TCModel private var editButton: JButton? = null init { - val direnv = sharedState.getUserData(DirenvService.STATE_KEY) ?: project?.let { DirenvService.getInstance(it).isEnabled } ?: DirenvState.Disabled - model = TCModel(getModelList(project, direnv)) + model = TCModel(getModelList(project, sharedState)) toolchainBox = TCComboBox(model) toolchainBox.addItemListener(::itemStateChanged) ZigToolchainListService.getInstance().addChangeListener(this) @@ -97,8 +97,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: override suspend fun toolchainListChanged() { withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { - val direnv = sharedState.getUserData(DirenvService.STATE_KEY) ?: project?.let { DirenvService.getInstance(it).isEnabled } ?: DirenvState.Disabled - val list = getModelList(project, direnv) + val list = getModelList(project, sharedState) model.updateContents(list) val onReload = selectOnNextReload selectOnNextReload = null @@ -129,9 +128,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: } override fun onUserDataChanged(key: Key<*>) { - if (key == DirenvService.STATE_KEY) { - zigCoroutineScope.launch { toolchainListChanged() } - } + zigCoroutineScope.launch { toolchainListChanged() } } @@ -199,13 +196,13 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: } -private fun getModelList(project: Project?, direnv: DirenvState): List { +private fun getModelList(project: Project?, data: UserDataHolder): List { val modelList = ArrayList() modelList.add(TCListElem.None) modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }.sortedBy { it.toolchain.name }) modelList.add(Separator("", true)) modelList.addAll(TCListElem.fetchGroup) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) - modelList.add(suggestZigToolchains(project, direnv).asPending()) + modelList.add(suggestZigToolchains(project, data).asPending()) return modelList } \ No newline at end of file From 1725b189a4d0ce9bc4dc831f8bcf29e8839c96c9 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 12:14:23 +0200 Subject: [PATCH 17/22] wire up synthetic library reloads to new config --- .../project/stdlib/ZigSyntheticLibrary.kt | 14 ++++++++++++-- .../project/toolchain/ZigToolchainService.kt | 10 +++++++++- .../toolchain/base/ZigToolchainConfigurable.kt | 2 -- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt index 4007c20a..83a5df0d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/stdlib/ZigSyntheticLibrary.kt @@ -35,6 +35,8 @@ import com.intellij.openapi.vfs.refreshAndFindVirtualDirectory import com.intellij.platform.backend.workspace.WorkspaceModel import com.intellij.platform.backend.workspace.toVirtualFileUrl import com.intellij.platform.workspace.jps.entities.* +import com.intellij.project.isDirectoryBased +import com.intellij.project.stateStore import com.intellij.workspaceModel.ide.legacyBridge.LegacyBridgeJpsEntitySourceFactory import kotlinx.coroutines.runBlocking import java.util.* @@ -75,7 +77,7 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen companion object { private const val ZIG_LIBRARY_ID = "Zig SDK" - private const val ZIG_MODULE_ID = "Zig" + private const val ZIG_MODULE_ID = "ZigBrains" suspend fun reload(project: Project, toolchain: ZigToolchain?) { val moduleId = ModuleId(ZIG_MODULE_ID) val workspaceModel = WorkspaceModel.getInstance(project) @@ -83,7 +85,15 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen val libRoot = LibraryRoot(root.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()), LibraryRootTypeId.SOURCES) val libraryTableId = LibraryTableId.ProjectLibraryTableId val libraryId = LibraryId(ZIG_LIBRARY_ID, libraryTableId) - val baseModuleDir = project.guessProjectDir()?.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()) ?: return + + var baseModuleDirFile: VirtualFile? = null + if (project.isDirectoryBased) { + baseModuleDirFile = project.stateStore.directoryStorePath?.refreshAndFindVirtualDirectory() + } + if (baseModuleDirFile == null) { + baseModuleDirFile = project.guessProjectDir() + } + val baseModuleDir = baseModuleDirFile?.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()) ?: return workspaceModel.update("Update Zig std") { builder -> builder.resolve(moduleId)?.let { moduleEntity -> builder.removeEntity(moduleEntity) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt index 5604af3b..a769cc1e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -22,7 +22,10 @@ package com.falsepattern.zigbrains.project.toolchain +import com.falsepattern.zigbrains.project.stdlib.ZigSyntheticLibrary import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.application.EDT import com.intellij.openapi.components.SerializablePersistentStateComponent import com.intellij.openapi.components.Service import com.intellij.openapi.components.State @@ -30,6 +33,8 @@ import com.intellij.openapi.components.Storage import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.util.xmlb.annotations.Attribute +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.UUID @Service(Service.Level.PROJECT) @@ -37,7 +42,7 @@ import java.util.UUID name = "ZigToolchain", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainService: SerializablePersistentStateComponent(State()), IZigToolchainService { +class ZigToolchainService(val project: Project): SerializablePersistentStateComponent(State()), IZigToolchainService { override var toolchainUUID: UUID? get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { if (ZigToolchainListService.getInstance().hasToolchain(it)) { @@ -53,6 +58,9 @@ class ZigToolchainService: SerializablePersistentStateComponent( } private var myView: ZigToolchainPanel? = null - var floating: Boolean = false - abstract fun createPanel(): ZigToolchainPanel override fun createOptionsPanel(): JComponent? { From ab20a57e9edf1610d9173d25520ba70e30ea875f Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 16:55:30 +0200 Subject: [PATCH 18/22] abstract away UUID storage for LSP code sharing --- .../toolchain/ZigToolchainListService.kt | 139 ++----------- .../project/toolchain/ZigToolchainService.kt | 4 +- .../project/toolchain/base/ZigToolchain.kt | 16 +- .../base/ZigToolchainConfigurable.kt | 4 +- .../toolchain/base/ZigToolchainProvider.kt | 6 +- .../toolchain/downloader/LocalSelector.kt | 8 +- .../toolchain/local/LocalZigToolchain.kt | 12 +- .../ui/ZigToolchainComboBoxHandler.kt | 6 +- .../toolchain/ui/ZigToolchainEditor.kt | 20 +- .../toolchain/ui/ZigToolchainListEditor.kt | 47 ++++- .../zigbrains/shared/NamedObject.kt | 32 +++ .../zigbrains/shared/UUIDMapSerializable.kt | 192 ++++++++++++++++++ 12 files changed, 323 insertions(+), 163 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index 8db422bd..7358067e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -22,125 +22,31 @@ package com.falsepattern.zigbrains.project.toolchain +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService.MyState import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.resolve import com.falsepattern.zigbrains.project.toolchain.base.toRef -import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.falsepattern.zigbrains.shared.AccessibleStorage +import com.falsepattern.zigbrains.shared.ChangeTrackingStorage +import com.falsepattern.zigbrains.shared.IterableStorage +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.falsepattern.zigbrains.shared.UUIDStorage import com.intellij.openapi.components.* -import kotlinx.coroutines.launch -import java.lang.ref.WeakReference -import java.util.UUID @Service(Service.Level.APP) @State( name = "ZigToolchainList", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainListService: SerializablePersistentStateComponent(State()), IZigToolchainListService { - private val changeListeners = ArrayList>() +class ZigToolchainListService: UUIDMapSerializable.Converting(MyState()), IZigToolchainListService { + override fun serialize(value: ZigToolchain) = value.toRef() + override fun deserialize(value: ZigToolchain.Ref) = value.resolve() + override fun getStorage(state: MyState) = state.toolchains + override fun updateStorage(state: MyState, storage: ToolchainStorage) = state.copy(toolchains = storage) - override val toolchains: Sequence> - get() = state.toolchains - .asSequence() - .mapNotNull { - val uuid = UUID.fromString(it.key) ?: return@mapNotNull null - val tc = it.value.resolve() ?: return@mapNotNull null - uuid to tc - } - - override fun setToolchain(uuid: UUID, toolchain: ZigToolchain) { - val str = uuid.toString() - val ref = toolchain.toRef() - updateState { - val newMap = HashMap() - newMap.putAll(it.toolchains) - newMap[str] = ref - it.copy(toolchains = newMap) - } - notifyChanged() - } - - override fun registerNewToolchain(toolchain: ZigToolchain): UUID { - val ref = toolchain.toRef() - var uuid = UUID.randomUUID() - updateState { - val newMap = HashMap() - newMap.putAll(it.toolchains) - var uuidStr = uuid.toString() - while (newMap.containsKey(uuidStr)) { - uuid = UUID.randomUUID() - uuidStr = uuid.toString() - } - newMap[uuidStr] = ref - it.copy(toolchains = newMap) - } - notifyChanged() - return uuid - } - - override fun getToolchain(uuid: UUID): ZigToolchain? { - return state.toolchains[uuid.toString()]?.resolve() - } - - override fun hasToolchain(uuid: UUID): Boolean { - return state.toolchains.containsKey(uuid.toString()) - } - - override fun removeToolchain(uuid: UUID) { - val str = uuid.toString() - updateState { - it.copy(toolchains = it.toolchains.filter { it.key != str }) - } - notifyChanged() - } - - override fun addChangeListener(listener: ToolchainListChangeListener) { - synchronized(changeListeners) { - changeListeners.add(WeakReference(listener)) - } - } - - override fun removeChangeListener(listener: ToolchainListChangeListener) { - synchronized(changeListeners) { - changeListeners.removeIf { - val v = it.get() - v == null || v === listener - } - } - } - - override fun withUniqueName(toolchain: T): T { - val baseName = toolchain.name ?: "" - var index = 0 - var currentName = baseName - while (toolchains.any { (_, existing) -> existing.name == currentName }) { - index++ - currentName = "$baseName ($index)" - } - @Suppress("UNCHECKED_CAST") - return toolchain.withName(currentName) as T - } - - private fun notifyChanged() { - synchronized(changeListeners) { - var i = 0 - while (i < changeListeners.size) { - val v = changeListeners[i].get() - if (v == null) { - changeListeners.removeAt(i) - continue - } - zigCoroutineScope.launch { - v.toolchainListChanged() - } - i++ - } - } - } - - data class State( + data class MyState( @JvmField - val toolchains: Map = emptyMap(), + val toolchains: ToolchainStorage = emptyMap(), ) companion object { @@ -149,19 +55,8 @@ class ZigToolchainListService: SerializablePersistentStateComponent> - fun setToolchain(uuid: UUID, toolchain: ZigToolchain) - fun registerNewToolchain(toolchain: ZigToolchain): UUID - fun getToolchain(uuid: UUID): ZigToolchain? - fun hasToolchain(uuid: UUID): Boolean - fun removeToolchain(uuid: UUID) - fun addChangeListener(listener: ToolchainListChangeListener) - fun removeChangeListener(listener: ToolchainListChangeListener) - fun withUniqueName(toolchain: T): T -} +sealed interface IZigToolchainListService: ChangeTrackingStorage, AccessibleStorage, IterableStorage + +private typealias ToolchainStorage = UUIDStorage diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt index a769cc1e..d8965bbc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -45,7 +45,7 @@ import java.util.UUID class ZigToolchainService(val project: Project): SerializablePersistentStateComponent(State()), IZigToolchainService { override var toolchainUUID: UUID? get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { - if (ZigToolchainListService.getInstance().hasToolchain(it)) { + if (it in zigToolchainList) { true } else { updateState { @@ -64,7 +64,7 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp } override val toolchain: ZigToolchain? - get() = toolchainUUID?.let { ZigToolchainListService.getInstance().getToolchain(it) } + get() = toolchainUUID?.let { zigToolchainList[it] } data class State( @JvmField diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt index 23aa9122..2769d274 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -23,8 +23,10 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool +import com.falsepattern.zigbrains.shared.NamedObject import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key import com.intellij.util.xmlb.annotations.Attribute import com.intellij.util.xmlb.annotations.MapAnnotation import java.nio.file.Path @@ -32,10 +34,15 @@ import java.nio.file.Path /** * These MUST be stateless and interchangeable! (e.g., immutable data class) */ -interface ZigToolchain { +interface ZigToolchain: NamedObject { val zig: ZigCompilerTool get() = ZigCompilerTool(this) - val name: String? + fun getUserData(key: Key): T? + + /** + * Returned type must be the same class + */ + fun withUserData(key: Key, value: T?): ZigToolchain fun workingDirectory(project: Project? = null): Path? @@ -43,11 +50,6 @@ interface ZigToolchain { fun pathToExecutable(toolName: String, project: Project? = null): Path - /** - * Returned object must be the same class. - */ - fun withName(newName: String?): ZigToolchain - data class Ref( @JvmField @Attribute diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt index 617c5a73..0746c934 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -23,10 +23,10 @@ package com.falsepattern.zigbrains.project.toolchain.base import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.NlsContexts import com.intellij.ui.dsl.builder.panel -import com.intellij.ui.util.minimumWidth import java.util.UUID import javax.swing.JComponent @@ -36,7 +36,7 @@ abstract class ZigToolchainConfigurable( ): NamedConfigurable() { var toolchain: T = tc set(value) { - ZigToolchainListService.getInstance().setToolchain(uuid, value) + zigToolchainList[uuid] = value field = value } private var myView: ZigToolchainPanel? = null diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt index c18a3923..58667862 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -22,8 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base -import com.falsepattern.zigbrains.direnv.DirenvState -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project import com.intellij.openapi.util.Key @@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flowOn import java.util.UUID +import kotlin.collections.none private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainProvider") @@ -70,7 +70,7 @@ fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<* @OptIn(ExperimentalCoroutinesApi::class) fun suggestZigToolchains(project: Project? = null, data: UserDataHolder = emptyData): Flow { - val existing = ZigToolchainListService.getInstance().toolchains.map { (_, tc) -> tc }.toList() + val existing = zigToolchainList.map { (_, tc) -> tc } return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext -> val compatibleExisting = existing.filter { ext.isCompatible(it) } val suggestions = ext.suggestToolchains(project, data) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt index fde0e7da..93553933 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt @@ -27,10 +27,12 @@ import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.withUniqueName import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.icons.AllIcons import com.intellij.openapi.application.ModalityState @@ -78,9 +80,7 @@ object LocalSelector { errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") dialog.setOkActionEnabled(false) } else { - val existingToolchain = ZigToolchainListService - .getInstance() - .toolchains + val existingToolchain = zigToolchainList .mapNotNull { it.second as? LocalZigToolchain } .firstOrNull { it.location == tc.location } if (existingToolchain != null) { @@ -95,7 +95,7 @@ object LocalSelector { } } if (tc != null) { - tc = ZigToolchainListService.getInstance().withUniqueName(tc) + tc = zigToolchainList.withUniqueName(tc) } val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() name.emptyText.text = tc?.name ?: "" diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index f5dc9045..70015f5f 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -28,13 +28,23 @@ import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir +import com.intellij.openapi.util.Key import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull +import com.intellij.util.keyFMap.KeyFMap import java.nio.file.Path @JvmRecord -data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain { +data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null, private val userData: KeyFMap = KeyFMap.EMPTY_MAP): ZigToolchain { + override fun getUserData(key: Key): T? { + return userData.get(key) + } + + override fun withUserData(key: Key, value: T?): LocalZigToolchain { + return copy(userData = if (value == null) userData.minus(key) else userData.plus(key, value)) + } + override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt index 3febf6bd..0b832a62 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt @@ -25,6 +25,8 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.withUniqueName import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import java.awt.Component import java.util.UUID @@ -32,8 +34,8 @@ import java.util.UUID internal object ZigToolchainComboBoxHandler { @RequiresBackgroundThread suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) { - is TCListElem.Toolchain.Suggested -> ZigToolchainListService.getInstance().withUniqueName(elem.toolchain) + is TCListElem.Toolchain.Suggested -> zigToolchainList.withUniqueName(elem.toolchain) is TCListElem.Download -> Downloader.downloadToolchain(context) is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context) - }?.let { ZigToolchainListService.getInstance().registerNewToolchain(it) } + }?.let { zigToolchainList.registerNew(it) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt index c30d8295..8d270c91 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -23,14 +23,13 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.direnv.DirenvService -import com.falsepattern.zigbrains.direnv.DirenvState import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.StorageChangeListener import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT @@ -48,6 +47,7 @@ import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.awt.event.ItemEvent @@ -55,16 +55,17 @@ import java.util.UUID import javax.swing.JButton import kotlin.collections.addAll -class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable, ToolchainListChangeListener, ZigProjectConfigurationProvider.UserDataListener { +class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable, ZigProjectConfigurationProvider.UserDataListener { private val toolchainBox: TCComboBox private var selectOnNextReload: UUID? = null private val model: TCModel private var editButton: JButton? = null + private val changeListener: StorageChangeListener = { this@ZigToolchainEditor.toolchainListChanged() } init { model = TCModel(getModelList(project, sharedState)) toolchainBox = TCComboBox(model) toolchainBox.addItemListener(::itemStateChanged) - ZigToolchainListService.getInstance().addChangeListener(this) + zigToolchainList.addChangeListener(changeListener) sharedState.addUserDataChangeListener(this) model.whenListChanged { if (toolchainBox.isPopupVisible) { @@ -89,13 +90,14 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: return zigCoroutineScope.launch(toolchainBox.asContextElement()) { val uuid = runCatching { ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) }.getOrNull() + delay(100) withEDTContext(toolchainBox.asContextElement()) { applyUUIDNowOrOnReload(uuid) } } } - override suspend fun toolchainListChanged() { + private suspend fun toolchainListChanged() { withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { val list = getModelList(project, sharedState) model.updateContents(list) @@ -143,7 +145,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e -> zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) { var selectedUUID = toolchainBox.selectedToolchain ?: return@launchWithEDT - val toolchain = ZigToolchainListService.getInstance().getToolchain(selectedUUID) ?: return@launchWithEDT + val toolchain = zigToolchainList[selectedUUID] ?: return@launchWithEDT val config = toolchain.createNamedConfigurable(selectedUUID) val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config) if (apply) { @@ -182,7 +184,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: } override fun dispose() { - ZigToolchainListService.getInstance().removeChangeListener(this) + zigToolchainList.removeChangeListener(changeListener) } override val newProjectBeforeInitSelector get() = true @@ -199,7 +201,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: private fun getModelList(project: Project?, data: UserDataHolder): List { val modelList = ArrayList() modelList.add(TCListElem.None) - modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }.sortedBy { it.toolchain.name }) + modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.toolchain.name }) modelList.add(Separator("", true)) modelList.addAll(TCListElem.fetchGroup) modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index e5fc8e71..7c39a202 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -23,11 +23,12 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.StorageChangeListener import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope @@ -39,14 +40,17 @@ import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.util.IconUtil import com.intellij.util.asSafely +import com.intellij.util.concurrency.annotations.RequiresEdt import kotlinx.coroutines.launch import java.util.UUID import javax.swing.JComponent import javax.swing.tree.DefaultTreeModel -class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeListener { +class ZigToolchainListEditor : MasterDetailsComponent() { private var isTreeInitialized = false private var registered: Boolean = false + private var selectOnNextReload: UUID? = null + private val changeListener: StorageChangeListener = { this@ZigToolchainListEditor.toolchainListChanged() } override fun createComponent(): JComponent { if (!isTreeInitialized) { @@ -54,7 +58,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList isTreeInitialized = true } if (!registered) { - ZigToolchainListService.getInstance().addChangeListener(this) + zigToolchainList.addChangeListener(changeListener) registered = true } return super.createComponent() @@ -81,7 +85,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList override fun onItemDeleted(item: Any?) { if (item is UUID) { - ZigToolchainListService.getInstance().removeToolchain(item) + zigToolchainList.remove(item) } super.onItemDeleted(item) } @@ -93,7 +97,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem) if (uuid != null) { withEDTContext(myWholePanel.asContextElement()) { - selectNodeInTree(uuid) + applyUUIDNowOrOnReload(uuid) } } } @@ -108,6 +112,13 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title") + override fun disposeUIResources() { + super.disposeUIResources() + if (registered) { + zigToolchainList.removeChangeListener(changeListener) + } + } + private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { val node = MyNode(toolchain.createNamedConfigurable(uuid)) addNode(node, myRoot) @@ -116,23 +127,37 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList private fun reloadTree() { val currentSelection = selectedObject?.asSafely() myRoot.removeAllChildren() - ZigToolchainListService.getInstance().toolchains.forEach { (uuid, toolchain) -> + val onReload = selectOnNextReload + selectOnNextReload = null + var hasOnReload = false + zigToolchainList.forEach { (uuid, toolchain) -> addToolchain(uuid, toolchain) + if (uuid == onReload) { + hasOnReload = true + } } (myTree.model as DefaultTreeModel).reload() + if (hasOnReload) { + selectNodeInTree(onReload) + return + } currentSelection?.let { selectNodeInTree(it) } } - override fun disposeUIResources() { - super.disposeUIResources() - if (registered) { - ZigToolchainListService.getInstance().removeChangeListener(this) + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + selectNodeInTree(uuid) + val currentSelection = selectedObject?.asSafely() + if (uuid != null && uuid != currentSelection) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null } } - override suspend fun toolchainListChanged() { + private suspend fun toolchainListChanged() { withEDTContext(myWholePanel.asContextElement()) { reloadTree() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt new file mode 100644 index 00000000..3bb35b82 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/NamedObject.kt @@ -0,0 +1,32 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +interface NamedObject> { + val name: String? + + /** + * Returned object must be the exact same class as the called one. + */ + fun withName(newName: String?): T +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt new file mode 100644 index 00000000..afc40ea4 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt @@ -0,0 +1,192 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +import com.intellij.openapi.components.SerializablePersistentStateComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.lang.ref.WeakReference +import java.util.UUID +import kotlin.collections.any + +typealias UUIDStorage = Map + +abstract class UUIDMapSerializable(init: S): SerializablePersistentStateComponent(init), ChangeTrackingStorage { + private val changeListeners = ArrayList>() + + protected abstract fun getStorage(state: S): UUIDStorage + + protected abstract fun updateStorage(state: S, storage: UUIDStorage): S + + override fun addChangeListener(listener: StorageChangeListener) { + synchronized(changeListeners) { + changeListeners.add(WeakReference(listener)) + } + } + + override fun removeChangeListener(listener: StorageChangeListener) { + synchronized(changeListeners) { + changeListeners.removeIf { + val v = it.get() + v == null || v === listener + } + } + } + + protected fun registerNewUUID(value: T): UUID { + var uuid = UUID.randomUUID() + updateState { + val newMap = HashMap() + newMap.putAll(getStorage(it)) + var uuidStr = uuid.toString() + while (newMap.containsKey(uuidStr)) { + uuid = UUID.randomUUID() + uuidStr = uuid.toString() + } + newMap[uuidStr] = value + updateStorage(it, newMap) + } + notifyChanged() + return uuid + } + + protected fun setStateUUID(uuid: UUID, value: T) { + val str = uuid.toString() + updateState { + val newMap = HashMap() + newMap.putAll(getStorage(it)) + newMap[str] = value + updateStorage(it, newMap) + } + notifyChanged() + } + + protected fun getStateUUID(uuid: UUID): T? { + return getStorage(state)[uuid.toString()] + } + + protected fun hasStateUUID(uuid: UUID): Boolean { + return getStorage(state).containsKey(uuid.toString()) + } + + protected fun removeStateUUID(uuid: UUID) { + val str = uuid.toString() + updateState { + updateStorage(state, getStorage(state).filter { it.key != str }) + } + notifyChanged() + } + + private fun notifyChanged() { + synchronized(changeListeners) { + var i = 0 + while (i < changeListeners.size) { + val v = changeListeners[i].get() + if (v == null) { + changeListeners.removeAt(i) + continue + } + zigCoroutineScope.launch { + v() + } + i++ + } + } + } + + abstract class Converting(init: S): + UUIDMapSerializable(init), + AccessibleStorage, + IterableStorage + { + protected abstract fun serialize(value: R): T + protected abstract fun deserialize(value: T): R? + override fun registerNew(value: R): UUID { + val ser = serialize(value) + return registerNewUUID(ser) + } + override operator fun set(uuid: UUID, value: R) { + val ser = serialize(value) + setStateUUID(uuid, ser) + } + override operator fun get(uuid: UUID): R? { + return getStateUUID(uuid)?.let { deserialize(it) } + } + override operator fun contains(uuid: UUID): Boolean { + return hasStateUUID(uuid) + } + override fun remove(uuid: UUID) { + removeStateUUID(uuid) + } + + override fun iterator(): Iterator> { + return getStorage(state) + .asSequence() + .mapNotNull { + val uuid = UUID.fromString(it.key) ?: return@mapNotNull null + val tc = deserialize(it.value) ?: return@mapNotNull null + uuid to tc + }.iterator() + } + } + + abstract class Direct(init: S): Converting(init) { + override fun serialize(value: T): T { + return value + } + + override fun deserialize(value: T): T? { + return value + } + } +} + +typealias StorageChangeListener = suspend CoroutineScope.() -> Unit + +interface ChangeTrackingStorage { + fun addChangeListener(listener: StorageChangeListener) + fun removeChangeListener(listener: StorageChangeListener) +} + +interface AccessibleStorage { + fun registerNew(value: R): UUID + operator fun set(uuid: UUID, value: R) + operator fun get(uuid: UUID): R? + operator fun contains(uuid: UUID): Boolean + fun remove(uuid: UUID) +} + +interface IterableStorage: Iterable> + +fun , T: R> IterableStorage.withUniqueName(value: T): T { + val baseName = value.name ?: "" + var index = 0 + var currentName = baseName + val names = this.map { (_, existing) -> existing.name } + while (names.any { it == currentName }) { + index++ + currentName = "$baseName ($index)" + } + @Suppress("UNCHECKED_CAST") + return value.withName(currentName) as T +} \ No newline at end of file From 137977f69119e912dce67da93e7e8c2d7fcbf23f Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 17:28:56 +0200 Subject: [PATCH 19/22] modular toolchain config --- .../toolchain/ZigToolchainListService.kt | 16 ++----- .../base/ZigToolchainConfigurable.kt | 27 +++++------ .../base/ZigToolchainExtensionsProvider.kt | 38 +++++++++++++++ .../toolchain/base/ZigToolchainPanel.kt | 30 ++++-------- .../toolchain/base/ZigToolchainPanelBase.kt | 46 +++++++++++++++++++ .../toolchain/local/LocalZigToolchainPanel.kt | 6 +-- 6 files changed, 112 insertions(+), 51 deletions(-) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt index 7358067e..b599b6cb 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainListService.kt @@ -26,9 +26,6 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService.MySt import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.resolve import com.falsepattern.zigbrains.project.toolchain.base.toRef -import com.falsepattern.zigbrains.shared.AccessibleStorage -import com.falsepattern.zigbrains.shared.ChangeTrackingStorage -import com.falsepattern.zigbrains.shared.IterableStorage import com.falsepattern.zigbrains.shared.UUIDMapSerializable import com.falsepattern.zigbrains.shared.UUIDStorage import com.intellij.openapi.components.* @@ -38,25 +35,20 @@ import com.intellij.openapi.components.* name = "ZigToolchainList", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainListService: UUIDMapSerializable.Converting(MyState()), IZigToolchainListService { +class ZigToolchainListService: UUIDMapSerializable.Converting(MyState()) { override fun serialize(value: ZigToolchain) = value.toRef() override fun deserialize(value: ZigToolchain.Ref) = value.resolve() override fun getStorage(state: MyState) = state.toolchains override fun updateStorage(state: MyState, storage: ToolchainStorage) = state.copy(toolchains = storage) - data class MyState( - @JvmField - val toolchains: ToolchainStorage = emptyMap(), - ) + data class MyState(@JvmField val toolchains: ToolchainStorage = emptyMap()) companion object { @JvmStatic - fun getInstance(): IZigToolchainListService = service() + fun getInstance(): ZigToolchainListService = service() } } -inline val zigToolchainList: IZigToolchainListService get() = ZigToolchainListService.getInstance() - -sealed interface IZigToolchainListService: ChangeTrackingStorage, AccessibleStorage, IterableStorage +inline val zigToolchainList: ZigToolchainListService get() = ZigToolchainListService.getInstance() private typealias ToolchainStorage = UUIDStorage 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 0746c934..9de48994 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -22,7 +22,6 @@ package com.falsepattern.zigbrains.project.toolchain.base -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.NlsContexts @@ -39,19 +38,21 @@ abstract class ZigToolchainConfigurable( zigToolchainList[uuid] = value field = value } - private var myView: ZigToolchainPanel? = null + private var myViews: List> = emptyList() abstract fun createPanel(): ZigToolchainPanel override fun createOptionsPanel(): JComponent? { - var view = myView - if (view == null) { - view = createPanel() - view.reset(toolchain) - myView = view + var views = myViews + if (views.isEmpty()) { + views = ArrayList>() + views.add(createPanel()) + views.addAll(createZigToolchainExtensionPanels()) + views.forEach { it.reset(toolchain) } + myViews = views } return panel { - view.attach(this) + views.forEach { it.attach(this@panel) } }.withMinimumWidth(20) } @@ -68,20 +69,20 @@ abstract class ZigToolchainConfigurable( } override fun isModified(): Boolean { - return myView?.isModified(toolchain) == true + return myViews.any { it.isModified(toolchain) } } override fun apply() { - myView?.apply(toolchain)?.let { toolchain = it } + toolchain = myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc } } override fun reset() { - myView?.reset(toolchain) + myViews.forEach { it.reset(toolchain) } } override fun disposeUIResources() { - myView?.dispose() - myView = null + myViews.forEach { it.dispose() } + myViews = emptyList() super.disposeUIResources() } } \ 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 new file mode 100644 index 00000000..75ba7c5e --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt @@ -0,0 +1,38 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.intellij.openapi.extensions.ExtensionPointName + +private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainExtensionsProvider") + +internal interface ZigToolchainExtensionsProvider { + fun createExtensionPanel(): ZigToolchainPanel? + val index: Int +} + +fun createZigToolchainExtensionPanels(): List> { + return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull { + it.createExtensionPanel() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt index ab765d0c..109f9c1a 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt @@ -22,29 +22,15 @@ package com.falsepattern.zigbrains.project.toolchain.base -import com.falsepattern.zigbrains.ZigBrainsBundle import com.intellij.openapi.Disposable -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 ZigToolchainPanel: Disposable { - private val nameField = JBTextField(25) - - protected var nameFieldValue: String? - get() = nameField.text.ifBlank { null } - set(value) {nameField.text = value ?: ""} - - open fun attach(p: Panel): Unit = with(p) { - row(ZigBrainsBundle.message("settings.toolchain.base.name.label")) { - cell(nameField).resizableColumn().align(AlignX.FILL) - } - separator() - } - - abstract fun isModified(toolchain: T): Boolean - abstract fun apply(toolchain: T): T? - abstract fun reset(toolchain: T) +interface ZigToolchainPanel: Disposable { + fun attach(p: Panel) + fun isModified(toolchain: T): Boolean + /** + * Returned object must be the exact same class as the provided one. + */ + fun apply(toolchain: T): T? + fun reset(toolchain: T) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt new file mode 100644 index 00000000..5d2543bd --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt @@ -0,0 +1,46 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.base + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.intellij.openapi.Disposable +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import com.intellij.ui.util.preferredHeight +import java.awt.Dimension + +abstract class ZigToolchainPanelBase: ZigToolchainPanel { + private val nameField = JBTextField(25) + + protected var nameFieldValue: String? + get() = nameField.text.ifBlank { null } + set(value) {nameField.text = value ?: ""} + + override fun attach(p: Panel): Unit = with(p) { + row(ZigBrainsBundle.message("settings.toolchain.base.name.label")) { + cell(nameField).resizableColumn().align(AlignX.FILL) + } + separator() + } +} \ No newline at end of file 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 111172c5..f5b4f612 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt @@ -23,10 +23,9 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainPanel +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainPanelBase import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.openapi.Disposable import com.intellij.openapi.application.ModalityState import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.util.Disposer @@ -35,7 +34,6 @@ 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.JBTextField import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel @@ -46,7 +44,7 @@ import kotlinx.coroutines.launch import javax.swing.event.DocumentEvent import kotlin.io.path.pathString -class LocalZigToolchainPanel() : ZigToolchainPanel() { +class LocalZigToolchainPanel() : ZigToolchainPanelBase() { private val pathToToolchain = textFieldWithBrowseButton( null, FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) From dcede7eb43ceec85e961ea63c7b5f3e2c6b834e2 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 20:10:27 +0200 Subject: [PATCH 20/22] initial LSP work --- build.gradle.kts | 6 +- .../zigbrains/direnv/ui/DirenvEditor.kt | 2 +- .../project/execution/base/ZigExecConfig.kt | 1 - .../project/settings/ZigConfigurable.kt | 2 - .../project/toolchain/ZigToolchainService.kt | 13 +- .../base/ZigToolchainConfigurable.kt | 7 +- .../base/ZigToolchainExtensionsProvider.kt | 5 +- .../toolchain/downloader/Downloader.kt | 1 - .../toolchain/downloader/ZigVersionInfo.kt | 2 - .../toolchain/local/LocalZigToolchainPanel.kt | 4 +- .../local/LocalZigToolchainProvider.kt | 1 - .../ImmutableElementPanel.kt} | 10 +- .../ImmutableNamedElementPanelBase.kt} | 8 +- .../ui/ZigToolchainComboBoxHandler.kt | 11 +- .../toolchain/ui/ZigToolchainDriver.kt | 92 +++++ .../toolchain/ui/ZigToolchainEditor.kt | 150 +------- .../toolchain/ui/ZigToolchainListEditor.kt | 138 +------ .../project/toolchain/ui/elements.kt | 62 --- .../zigbrains/project/toolchain/ui/model.kt | 254 ++----------- .../zigbrains/shared/cli/CLIUtil.kt | 1 - .../zigbrains/shared/ui/UUIDComboBoxDriver.kt | 37 ++ .../zigbrains/shared/ui/UUIDMapEditor.kt | 155 ++++++++ .../zigbrains/shared/ui/UUIDMapSelector.kt | 161 ++++++++ .../zigbrains/shared/ui/elements.kt | 82 ++++ .../falsepattern/zigbrains/shared/ui/model.kt | 255 +++++++++++++ .../resources/zigbrains/Bundle.properties | 6 +- .../lsp/ToolchainZLSConfigProvider.kt | 24 +- .../lsp/ZLSProjectConfigurationProvider.kt | 47 --- .../falsepattern/zigbrains/lsp/ZLSStartup.kt | 21 +- .../lsp/ZLSStreamConnectionProvider.kt | 34 +- .../zigbrains/lsp/ZigLanguageServerFactory.kt | 26 +- .../ZigEditorNotificationProvider.kt | 8 +- .../lsp/settings/ZLSProjectSettingsService.kt | 158 -------- .../zigbrains/lsp/settings/ZLSSettings.kt | 52 ++- .../lsp/settings/ZLSSettingsConfigProvider.kt | 3 +- .../lsp/settings/ZLSSettingsConfigurable.kt | 57 --- .../lsp/settings/ZLSSettingsPanel.kt | 354 ------------------ .../zigbrains/lsp/zls/ZLSConfigurable.kt | 85 +++++ .../lsp/zls/ZLSInstallationsService.kt | 51 +++ .../zigbrains/lsp/zls/ZLSPanel.kt | 131 +++++++ .../zigbrains/lsp/zls/ZLSService.kt | 63 ++++ .../zigbrains/lsp/zls/ZLSVersion.kt | 65 ++++ .../zigbrains/lsp/zls/ui/ZLSDriver.kt | 67 ++++ .../zigbrains/lsp/zls/ui/ZLSEditor.kt | 89 +++++ .../zigbrains/lsp/zls/ui/ZLSListEditor.kt | 39 ++ .../zigbrains/lsp/zls/ui/model.kt | 92 +++++ .../main/resources/META-INF/zigbrains-lsp.xml | 9 +- .../resources/zigbrains/lsp/Bundle.properties | 7 + src/main/resources/META-INF/plugin.xml | 5 + 49 files changed, 1611 insertions(+), 1342 deletions(-) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{base/ZigToolchainPanel.kt => ui/ImmutableElementPanel.kt} (82%) rename core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/{base/ZigToolchainPanelBase.kt => ui/ImmutableNamedElementPanelBase.kt} (85%) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt delete mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt delete mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt delete mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt delete mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt diff --git a/build.gradle.kts b/build.gradle.kts index 04c2476b..9f120e29 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,7 +83,7 @@ allprojects { } } filter { -// includeModule("com.redhat.devtools.intellij", "lsp4ij") + includeModule("com.redhat.devtools.intellij", "lsp4ij") } } mavenCentral() @@ -104,12 +104,12 @@ dependencies { pluginVerifier() zipSigner() -// plugin(lsp4ijPluginString) + plugin(lsp4ijPluginString) } runtimeOnly(project(":core")) runtimeOnly(project(":cidr")) -// runtimeOnly(project(":lsp")) + runtimeOnly(project(":lsp")) } intellijPlatform { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt index e0231243..3aa15bb8 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/direnv/ui/DirenvEditor.kt @@ -92,6 +92,6 @@ abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationP } override val index: Int - get() = 1 + get() = 100 } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt index c28926cf..4fc2a466 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigExecConfig.kt @@ -35,7 +35,6 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.util.NlsActions.ActionText import com.intellij.openapi.vfs.toNioPathOrNull -import com.intellij.platform.util.progress.reportRawProgress import org.jdom.Element import org.jetbrains.annotations.Nls diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt index 6ff153e6..57b53003 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigConfigurable.kt @@ -25,8 +25,6 @@ package com.falsepattern.zigbrains.project.settings import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.shared.SubConfigurable import com.intellij.openapi.project.Project -import com.intellij.openapi.util.NlsContexts -import com.intellij.openapi.util.UserDataHolderBase class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter() { override fun instantiate(): List> { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt index d8965bbc..6cf457fe 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -42,8 +42,8 @@ import java.util.UUID name = "ZigToolchain", storages = [Storage("zigbrains.xml")] ) -class ZigToolchainService(val project: Project): SerializablePersistentStateComponent(State()), IZigToolchainService { - override var toolchainUUID: UUID? +class ZigToolchainService(private val project: Project): SerializablePersistentStateComponent(State()) { + var toolchainUUID: UUID? get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { if (it in zigToolchainList) { true @@ -63,7 +63,7 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp } } - override val toolchain: ZigToolchain? + val toolchain: ZigToolchain? get() = toolchainUUID?.let { zigToolchainList[it] } data class State( @@ -74,11 +74,6 @@ class ZigToolchainService(val project: Project): SerializablePersistentStateComp companion object { @JvmStatic - fun getInstance(project: Project): IZigToolchainService = project.service() + fun getInstance(project: Project): ZigToolchainService = project.service() } -} - -sealed interface IZigToolchainService { - var toolchainUUID: UUID? - val toolchain: ZigToolchain? } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt index 9de48994..c39841f4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.ui.NamedConfigurable import com.intellij.openapi.util.NlsContexts @@ -38,14 +39,14 @@ abstract class ZigToolchainConfigurable( zigToolchainList[uuid] = value field = value } - private var myViews: List> = emptyList() + private var myViews: List> = emptyList() - abstract fun createPanel(): ZigToolchainPanel + abstract fun createPanel(): ImmutableElementPanel override fun createOptionsPanel(): JComponent? { var views = myViews if (views.isEmpty()) { - views = ArrayList>() + views = ArrayList>() views.add(createPanel()) views.addAll(createZigToolchainExtensionPanels()) views.forEach { it.reset(toolchain) } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt index 75ba7c5e..e0be25d6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt @@ -22,16 +22,17 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.intellij.openapi.extensions.ExtensionPointName private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.toolchainExtensionsProvider") internal interface ZigToolchainExtensionsProvider { - fun createExtensionPanel(): ZigToolchainPanel? + fun createExtensionPanel(): ImmutableElementPanel? val index: Int } -fun createZigToolchainExtensionPanels(): List> { +fun createZigToolchainExtensionPanels(): List> { return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull { it.createExtensionPanel() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt index 6183b3a0..5a7522e9 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt @@ -49,7 +49,6 @@ import com.intellij.util.asSafely import com.intellij.util.concurrency.annotations.RequiresEdt import java.awt.Component import java.nio.file.Path -import java.util.* import javax.swing.DefaultComboBoxModel import javax.swing.JList import javax.swing.event.DocumentEvent diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt index 87e80f67..8b9d54b6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt @@ -50,10 +50,8 @@ import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromStream import java.io.File -import java.lang.IllegalStateException import java.nio.file.Files import java.nio.file.Path -import java.util.* import kotlin.io.path.ExperimentalPathApi import kotlin.io.path.deleteRecursively import kotlin.io.path.isDirectory diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt index f5b4f612..85736d5e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainPanel.kt @@ -23,7 +23,7 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainPanelBase +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.application.ModalityState @@ -44,7 +44,7 @@ import kotlinx.coroutines.launch import javax.swing.event.DocumentEvent import kotlin.io.path.pathString -class LocalZigToolchainPanel() : ZigToolchainPanelBase() { +class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase() { private val pathToToolchain = textFieldWithBrowseButton( null, FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index 49b223d7..14a79acc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -23,7 +23,6 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.direnv.DirenvService -import com.falsepattern.zigbrains.direnv.DirenvState import com.falsepattern.zigbrains.direnv.Env import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt similarity index 82% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt index 109f9c1a..2737529e 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanel.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableElementPanel.kt @@ -20,17 +20,17 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.base +package com.falsepattern.zigbrains.project.toolchain.ui import com.intellij.openapi.Disposable import com.intellij.ui.dsl.builder.Panel -interface ZigToolchainPanel: Disposable { +interface ImmutableElementPanel: Disposable { fun attach(p: Panel) - fun isModified(toolchain: T): Boolean + fun isModified(elem: T): Boolean /** * Returned object must be the exact same class as the provided one. */ - fun apply(toolchain: T): T? - fun reset(toolchain: T) + fun apply(elem: T): T? + fun reset(elem: T) } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt similarity index 85% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt index 5d2543bd..d19571f6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainPanelBase.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ImmutableNamedElementPanelBase.kt @@ -20,17 +20,15 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.base +package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle -import com.intellij.openapi.Disposable +import com.falsepattern.zigbrains.shared.NamedObject import com.intellij.ui.components.JBTextField import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.util.preferredHeight -import java.awt.Dimension -abstract class ZigToolchainPanelBase: ZigToolchainPanel { +abstract class ImmutableNamedElementPanelBase: ImmutableElementPanel { private val nameField = JBTextField(25) protected var nameFieldValue: String? diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt index 0b832a62..ffc5a025 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt @@ -22,10 +22,11 @@ package com.falsepattern.zigbrains.project.toolchain.ui -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.downloader.Downloader import com.falsepattern.zigbrains.project.toolchain.downloader.LocalSelector import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.ui.ListElem import com.falsepattern.zigbrains.shared.withUniqueName import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import java.awt.Component @@ -33,9 +34,9 @@ import java.util.UUID internal object ZigToolchainComboBoxHandler { @RequiresBackgroundThread - suspend fun onItemSelected(context: Component, elem: TCListElem.Pseudo): UUID? = when(elem) { - is TCListElem.Toolchain.Suggested -> zigToolchainList.withUniqueName(elem.toolchain) - is TCListElem.Download -> Downloader.downloadToolchain(context) - is TCListElem.FromDisk -> LocalSelector.browseFromDisk(context) + suspend fun onItemSelected(context: Component, elem: ListElem.Pseudo): UUID? = when(elem) { + is ListElem.One.Suggested -> zigToolchainList.withUniqueName(elem.instance) + is ListElem.Download -> Downloader.downloadToolchain(context) + is ListElem.FromDisk -> LocalSelector.browseFromDisk(context) }?.let { zigToolchainList.registerNew(it) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt new file mode 100644 index 00000000..f8188478 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt @@ -0,0 +1,92 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable +import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ListElemIn +import com.falsepattern.zigbrains.shared.ui.Separator +import com.falsepattern.zigbrains.shared.ui.UUIDComboBoxDriver +import com.falsepattern.zigbrains.shared.ui.ZBComboBox +import com.falsepattern.zigbrains.shared.ui.ZBContext +import com.falsepattern.zigbrains.shared.ui.ZBModel +import com.falsepattern.zigbrains.shared.ui.asActual +import com.falsepattern.zigbrains.shared.ui.asPending +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.UserDataHolder +import java.awt.Component +import java.util.UUID + +sealed interface ZigToolchainDriver: UUIDComboBoxDriver { + override val theMap get() = zigToolchainList + + override fun createContext(model: ZBModel): ZBContext { + return TCContext(null, model) + } + + override fun createComboBox(model: ZBModel): ZBComboBox { + return TCComboBox(model) + } + + override suspend fun resolvePseudo( + context: Component, + elem: ListElem.Pseudo + ): UUID? { + return ZigToolchainComboBoxHandler.onItemSelected(context, elem) + } + + override fun createNamedConfigurable( + uuid: UUID, + elem: ZigToolchain + ): NamedConfigurable { + return elem.createNamedConfigurable(uuid) + } + + object ForList: ZigToolchainDriver { + override fun constructModelList(): List> { + val modelList = ArrayList>() + modelList.addAll(ListElem.fetchGroup()) + modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) + modelList.add(suggestZigToolchains().asPending()) + return modelList + } + } + + class ForSelector(val project: Project?, val data: UserDataHolder): ZigToolchainDriver { + override fun constructModelList(): List> { + val modelList = ArrayList>() + modelList.add(ListElem.None()) + modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.instance.name }) + modelList.add(Separator("", true)) + modelList.addAll(ListElem.fetchGroup()) + modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) + modelList.add(suggestZigToolchains(project, data).asPending()) + return modelList + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt index 8d270c91..12ac6894 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -24,113 +24,29 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService -import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains -import com.falsepattern.zigbrains.project.toolchain.zigToolchainList -import com.falsepattern.zigbrains.shared.StorageChangeListener +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.shared.SubConfigurable -import com.falsepattern.zigbrains.shared.coroutine.asContextElement -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.openapi.application.EDT -import com.intellij.openapi.observable.util.whenListChanged -import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.openapi.project.ProjectManager -import com.intellij.openapi.ui.DialogWrapper import com.intellij.openapi.util.Key -import com.intellij.openapi.util.UserDataHolder -import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel -import com.intellij.util.concurrency.annotations.RequiresEdt -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import java.awt.event.ItemEvent -import java.util.UUID -import javax.swing.JButton -import kotlin.collections.addAll -class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable, ZigProjectConfigurationProvider.UserDataListener { - private val toolchainBox: TCComboBox - private var selectOnNextReload: UUID? = null - private val model: TCModel - private var editButton: JButton? = null - private val changeListener: StorageChangeListener = { this@ZigToolchainEditor.toolchainListChanged() } +class ZigToolchainEditor(private var project: Project?, + private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): + UUIDMapSelector(ZigToolchainDriver.ForSelector(project, sharedState)), + SubConfigurable, + ZigProjectConfigurationProvider.UserDataListener +{ init { - model = TCModel(getModelList(project, sharedState)) - toolchainBox = TCComboBox(model) - toolchainBox.addItemListener(::itemStateChanged) - zigToolchainList.addChangeListener(changeListener) sharedState.addUserDataChangeListener(this) - model.whenListChanged { - if (toolchainBox.isPopupVisible) { - toolchainBox.isPopupVisible = false - toolchainBox.isPopupVisible = true - } - } - } - - private fun refreshButtonState(item: Any?) { - editButton?.isEnabled = item is TCListElem.Toolchain.Actual - editButton?.repaint() - } - - private fun itemStateChanged(event: ItemEvent) { - if (event.stateChange != ItemEvent.SELECTED) { - return - } - val item = event.item - refreshButtonState(item) - if (item !is TCListElem.Pseudo) - return - zigCoroutineScope.launch(toolchainBox.asContextElement()) { - val uuid = runCatching { ZigToolchainComboBoxHandler.onItemSelected(toolchainBox, item) }.getOrNull() - delay(100) - withEDTContext(toolchainBox.asContextElement()) { - applyUUIDNowOrOnReload(uuid) - } - } - } - - private suspend fun toolchainListChanged() { - withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { - val list = getModelList(project, sharedState) - model.updateContents(list) - val onReload = selectOnNextReload - selectOnNextReload = null - if (onReload != null) { - val element = list.firstOrNull { when(it) { - is TCListElem.Toolchain.Actual -> it.uuid == onReload - else -> false - } } - model.selectedItem = element - return@withContext - } - val selected = model.selected - if (selected != null && list.contains(selected)) { - model.selectedItem = selected - return@withContext - } - if (selected is TCListElem.Toolchain.Actual) { - val uuid = selected.uuid - val element = list.firstOrNull { when(it) { - is TCListElem.Toolchain.Actual -> it.uuid == uuid - else -> false - } } - model.selectedItem = element - return@withContext - } - model.selectedItem = TCListElem.None - } } override fun onUserDataChanged(key: Key<*>) { - zigCoroutineScope.launch { toolchainListChanged() } + zigCoroutineScope.launch { listChanged() } } @@ -141,50 +57,26 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: else "settings.toolchain.editor.toolchain.label") ) { - cell(toolchainBox).resizableColumn().align(AlignX.FILL) - button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e -> - zigCoroutineScope.launchWithEDT(toolchainBox.asContextElement()) { - var selectedUUID = toolchainBox.selectedToolchain ?: return@launchWithEDT - val toolchain = zigToolchainList[selectedUUID] ?: return@launchWithEDT - val config = toolchain.createNamedConfigurable(selectedUUID) - val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(toolchainBox)?.contentPane, config) - if (apply) { - applyUUIDNowOrOnReload(selectedUUID) - } - } - }.component.let { - editButton = it - refreshButtonState(toolchainBox.selectedItem) - } - } - } - - @RequiresEdt - private fun applyUUIDNowOrOnReload(uuid: UUID?) { - toolchainBox.selectedToolchain = uuid - if (uuid != null && toolchainBox.selectedToolchain == null) { - selectOnNextReload = uuid - } else { - selectOnNextReload = null + attachComboBoxRow(this) } } override fun isModified(context: Project): Boolean { - return ZigToolchainService.getInstance(context).toolchainUUID != toolchainBox.selectedToolchain + return ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID } override fun apply(context: Project) { - ZigToolchainService.getInstance(context).toolchainUUID = toolchainBox.selectedToolchain + ZigToolchainService.getInstance(context).toolchainUUID = selectedUUID } override fun reset(context: Project?) { val project = context ?: ProjectManager.getInstance().defaultProject - toolchainBox.selectedToolchain = ZigToolchainService.getInstance(project).toolchainUUID - refreshButtonState(toolchainBox.selectedItem) + selectedUUID = ZigToolchainService.getInstance(project).toolchainUUID } override fun dispose() { - zigToolchainList.removeChangeListener(changeListener) + super.dispose() + sharedState.removeUserDataChangeListener(this) } override val newProjectBeforeInitSelector get() = true @@ -195,16 +87,4 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState: override val index: Int get() = 0 } -} - - -private fun getModelList(project: Project?, data: UserDataHolder): List { - val modelList = ArrayList() - modelList.add(TCListElem.None) - modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.toolchain.name }) - modelList.add(Separator("", true)) - modelList.addAll(TCListElem.fetchGroup) - modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) - modelList.add(suggestZigToolchains(project, data).asPending()) - return modelList } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt index 7c39a202..7dd9131d 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainListEditor.kt @@ -23,143 +23,9 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains -import com.falsepattern.zigbrains.project.toolchain.zigToolchainList -import com.falsepattern.zigbrains.shared.StorageChangeListener -import com.falsepattern.zigbrains.shared.coroutine.asContextElement -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.Presentation -import com.intellij.openapi.observable.util.whenListChanged -import com.intellij.openapi.project.DumbAwareAction -import com.intellij.openapi.ui.MasterDetailsComponent -import com.intellij.util.IconUtil -import com.intellij.util.asSafely -import com.intellij.util.concurrency.annotations.RequiresEdt -import kotlinx.coroutines.launch -import java.util.UUID -import javax.swing.JComponent -import javax.swing.tree.DefaultTreeModel - -class ZigToolchainListEditor : MasterDetailsComponent() { - private var isTreeInitialized = false - private var registered: Boolean = false - private var selectOnNextReload: UUID? = null - private val changeListener: StorageChangeListener = { this@ZigToolchainListEditor.toolchainListChanged() } - - override fun createComponent(): JComponent { - if (!isTreeInitialized) { - initTree() - isTreeInitialized = true - } - if (!registered) { - zigToolchainList.addChangeListener(changeListener) - registered = true - } - return super.createComponent() - } - - override fun createActions(fromPopup: Boolean): List { - val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.toolchain.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) { - override fun actionPerformed(e: AnActionEvent) { - val modelList = ArrayList() - modelList.addAll(TCListElem.fetchGroup) - modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) - modelList.add(suggestZigToolchains().asPending()) - val model = TCModel(modelList) - val context = TCContext(null, model) - val popup = TCComboBoxPopup(context, null, ::onItemSelected) - model.whenListChanged { - popup.syncWithModelChange() - } - popup.showInBestPositionFor(e.dataContext) - } - } - return listOf(add, MyDeleteAction()) - } - - override fun onItemDeleted(item: Any?) { - if (item is UUID) { - zigToolchainList.remove(item) - } - super.onItemDeleted(item) - } - - private fun onItemSelected(elem: TCListElem) { - if (elem !is TCListElem.Pseudo) - return - zigCoroutineScope.launch(myWholePanel.asContextElement()) { - val uuid = ZigToolchainComboBoxHandler.onItemSelected(myWholePanel, elem) - if (uuid != null) { - withEDTContext(myWholePanel.asContextElement()) { - applyUUIDNowOrOnReload(uuid) - } - } - } - } - - override fun reset() { - reloadTree() - super.reset() - } - - override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.toolchain.list.empty") +import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +class ZigToolchainListEditor : UUIDMapEditor(ZigToolchainDriver.ForList) { override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title") - - override fun disposeUIResources() { - super.disposeUIResources() - if (registered) { - zigToolchainList.removeChangeListener(changeListener) - } - } - - private fun addToolchain(uuid: UUID, toolchain: ZigToolchain) { - val node = MyNode(toolchain.createNamedConfigurable(uuid)) - addNode(node, myRoot) - } - - private fun reloadTree() { - val currentSelection = selectedObject?.asSafely() - myRoot.removeAllChildren() - val onReload = selectOnNextReload - selectOnNextReload = null - var hasOnReload = false - zigToolchainList.forEach { (uuid, toolchain) -> - addToolchain(uuid, toolchain) - if (uuid == onReload) { - hasOnReload = true - } - } - (myTree.model as DefaultTreeModel).reload() - if (hasOnReload) { - selectNodeInTree(onReload) - return - } - currentSelection?.let { - selectNodeInTree(it) - } - } - - @RequiresEdt - private fun applyUUIDNowOrOnReload(uuid: UUID?) { - selectNodeInTree(uuid) - val currentSelection = selectedObject?.asSafely() - if (uuid != null && uuid != currentSelection) { - selectOnNextReload = uuid - } else { - selectOnNextReload = null - } - } - - private suspend fun toolchainListChanged() { - withEDTContext(myWholePanel.asContextElement()) { - reloadTree() - } - } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt deleted file mode 100644 index ab014123..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/elements.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain.ui - -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import java.util.UUID - - -internal sealed interface TCListElemIn -internal sealed interface TCListElem : TCListElemIn { - sealed interface Pseudo: TCListElem - sealed interface Toolchain : TCListElem { - val toolchain: ZigToolchain - - @JvmRecord - data class Suggested(override val toolchain: ZigToolchain): Toolchain, Pseudo - - @JvmRecord - data class Actual(val uuid: UUID, override val toolchain: ZigToolchain): Toolchain - } - object None: TCListElem - object Download : TCListElem, Pseudo - object FromDisk : TCListElem, Pseudo - data class Pending(val elems: Flow): TCListElem - - companion object { - val fetchGroup get() = listOf(Download, FromDisk) - } -} - -@JvmRecord -internal data class Separator(val text: String, val line: Boolean) : TCListElemIn - -internal fun Pair.asActual() = TCListElem.Toolchain.Actual(first, second) - -internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this) - -internal fun Flow.asPending() = TCListElem.Pending(map { it.asSuggested() }) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt index 2f490d13..8945a8d3 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/model.kt @@ -24,269 +24,61 @@ package com.falsepattern.zigbrains.project.toolchain.ui import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.render -import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer +import com.falsepattern.zigbrains.shared.ui.ZBComboBox +import com.falsepattern.zigbrains.shared.ui.ZBContext +import com.falsepattern.zigbrains.shared.ui.ZBModel import com.intellij.icons.AllIcons -import com.intellij.openapi.application.EDT -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.application.asContextElement -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.ComboBox -import com.intellij.ui.CellRendererPanel -import com.intellij.ui.CollectionComboBoxModel -import com.intellij.ui.ColoredListCellRenderer -import com.intellij.ui.GroupHeaderSeparator -import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleTextAttributes -import com.intellij.ui.components.panels.OpaquePanel -import com.intellij.ui.popup.list.ComboBoxPopup -import com.intellij.util.Consumer -import com.intellij.util.concurrency.annotations.RequiresEdt -import com.intellij.util.ui.EmptyIcon -import com.intellij.util.ui.JBUI -import com.intellij.util.ui.UIUtil -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.awt.BorderLayout -import java.awt.Component -import java.util.IdentityHashMap -import java.util.UUID -import javax.accessibility.AccessibleContext +import com.intellij.ui.icons.EMPTY_ICON import javax.swing.JList -import javax.swing.border.Border -internal class TCComboBoxPopup( - context: TCContext, - selected: TCListElem?, - onItemSelected: Consumer, -) : ComboBoxPopup(context, selected, onItemSelected) +class TCComboBox(model: ZBModel): ZBComboBox(model, ::TCCellRenderer) -internal class TCComboBox(model: TCModel): ComboBox(model) { - init { - setRenderer(TCCellRenderer { model }) - } - - var selectedToolchain: UUID? - set(value) { - if (value == null) { - selectedItem = TCListElem.None - return - } - for (i in 0.. item.uuid - else -> null - } - } -} - -internal class TCModel private constructor(elements: List, private var separators: MutableMap) : CollectionComboBoxModel(elements) { - private var counter: Int = 0 - companion object { - operator fun invoke(input: List): TCModel { - val (elements, separators) = convert(input) - val model = TCModel(elements, separators) - model.launchPendingResolve() - return model - } - - private fun convert(input: List): Pair, MutableMap> { - val separators = IdentityHashMap() - var lastSeparator: Separator? = null - val elements = ArrayList() - input.forEach { - when (it) { - is TCListElem -> { - if (lastSeparator != null) { - separators[it] = lastSeparator - lastSeparator = null - } - elements.add(it) - } - - is Separator -> lastSeparator = it - } - } - return elements to separators - } - } - - fun separatorAbove(elem: TCListElem) = separators[elem] - - private fun launchPendingResolve() { - runInEdt(ModalityState.any()) { - val counter = this.counter - val size = this.size - for (i in 0.. - insertBefore(elem, newElem, counter) - } - remove(elem, counter) - } - } - } - } - - @RequiresEdt - private fun remove(old: TCListElem, oldCounter: Int) { - val newCounter = this@TCModel.counter - if (oldCounter != newCounter) { - return - } - val index = this@TCModel.getElementIndex(old) - this@TCModel.remove(index) - val sep = separators.remove(old) - if (sep != null && this@TCModel.size > index) { - this@TCModel.getElementAt(index)?.let { separators[it] = sep } - } - } - - @RequiresEdt - private fun insertBefore(old: TCListElem, new: TCListElem?, oldCounter: Int) { - val newCounter = this@TCModel.counter - if (oldCounter != newCounter) { - return - } - if (new == null) { - return - } - val currentIndex = this@TCModel.getElementIndex(old) - separators.remove(old)?.let { - separators.put(new, it) - } - this@TCModel.add(currentIndex, new) - } - - @RequiresEdt - fun updateContents(input: List) { - counter++ - val (elements, separators) = convert(input) - this.separators = separators - replaceAll(elements) - launchPendingResolve() - } -} - -internal class TCContext(private val project: Project?, private val model: TCModel) : ComboBoxPopup.Context { - override fun getProject(): Project? { - return project - } - - override fun getModel(): TCModel { - return model - } - - override fun getRenderer(): TCCellRenderer { - return TCCellRenderer(::getModel) - } -} - -internal class TCCellRenderer(val getModel: () -> TCModel) : ColoredListCellRenderer() { - - override fun getListCellRendererComponent( - list: JList?, - value: TCListElem?, - index: Int, - selected: Boolean, - hasFocus: Boolean - ): Component? { - val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent - val panel = object : CellRendererPanel(BorderLayout()) { - val myContext = component.accessibleContext - - override fun getAccessibleContext(): AccessibleContext? { - return myContext - } - - override fun setBorder(border: Border?) { - component.border = border - } - } - panel.add(component, BorderLayout.CENTER) - - component.isOpaque = true - list?.let { background = if (selected) it.selectionBackground else it.background } - - val model = getModel() - - if (index == -1) { - component.isOpaque = false - panel.isOpaque = false - return panel - } - - val separator = value?.let { model.separatorAbove(it) } - - if (separator != null) { - val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 - val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) - separatorComponent.isHideLine = !separator.line - separatorComponent.caption = separator.text.ifBlank { null } - val wrapper = OpaquePanel(BorderLayout()) - wrapper.add(separatorComponent, BorderLayout.CENTER) - list?.let { wrapper.background = it.background } - panel.add(wrapper, BorderLayout.NORTH) - } - - return panel - } +class TCContext(project: Project?, model: ZBModel): ZBContext(project, model, ::TCCellRenderer) +class TCCellRenderer(getModel: () -> ZBModel): ZBCellRenderer(getModel) { override fun customizeCellRenderer( - list: JList, - value: TCListElem?, + list: JList?>, + value: ListElem?, index: Int, selected: Boolean, hasFocus: Boolean ) { icon = EMPTY_ICON when (value) { - is TCListElem.Toolchain -> { + is ListElem.One -> { val (icon, isSuggestion) = when(value) { - is TCListElem.Toolchain.Suggested -> AllIcons.General.Information to true - is TCListElem.Toolchain.Actual -> Icons.Zig to false + is ListElem.One.Suggested -> AllIcons.General.Information to true + is ListElem.One.Actual -> Icons.Zig to false } this.icon = icon - val toolchain = value.toolchain - toolchain.render(this, isSuggestion, index == -1) + val item = value.instance + item.render(this, isSuggestion, index == -1) } - is TCListElem.Download -> { + is ListElem.Download -> { icon = AllIcons.Actions.Download append(ZigBrainsBundle.message("settings.toolchain.model.download.text")) } - is TCListElem.FromDisk -> { + is ListElem.FromDisk -> { icon = AllIcons.General.OpenDisk append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text")) } - is TCListElem.Pending -> { + is ListElem.Pending -> { icon = AllIcons.Empty - append("Loading\u2026", SimpleTextAttributes.GRAYED_ATTRIBUTES) + append(ZigBrainsBundle.message("settings.toolchain.model.loading.text"), SimpleTextAttributes.GRAYED_ATTRIBUTES) } - is TCListElem.None, null -> { + is ListElem.None, null -> { icon = AllIcons.General.BalloonError append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) } } } -} -private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt index e1c48c16..d8042fae 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt @@ -28,7 +28,6 @@ import com.falsepattern.zigbrains.shared.ipc.IPCUtil import com.falsepattern.zigbrains.shared.ipc.ipc import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.openapi.options.ConfigurationException diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt new file mode 100644 index 00000000..7a735a6b --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDComboBoxDriver.kt @@ -0,0 +1,37 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.intellij.openapi.ui.NamedConfigurable +import java.awt.Component +import java.util.UUID + +interface UUIDComboBoxDriver { + val theMap: UUIDMapSerializable.Converting + fun constructModelList(): List> + fun createContext(model: ZBModel): ZBContext + fun createComboBox(model: ZBModel): ZBComboBox + suspend fun resolvePseudo(context: Component, elem: ListElem.Pseudo): UUID? + fun createNamedConfigurable(uuid: UUID, elem: T): NamedConfigurable +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt new file mode 100644 index 00000000..75eb7620 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapEditor.kt @@ -0,0 +1,155 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.shared.StorageChangeListener +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.Presentation +import com.intellij.openapi.observable.util.whenListChanged +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.openapi.ui.MasterDetailsComponent +import com.intellij.util.IconUtil +import com.intellij.util.asSafely +import com.intellij.util.concurrency.annotations.RequiresEdt +import kotlinx.coroutines.launch +import java.util.UUID +import javax.swing.JComponent +import javax.swing.tree.DefaultTreeModel + +abstract class UUIDMapEditor(val driver: UUIDComboBoxDriver): MasterDetailsComponent() { + private var isTreeInitialized = false + private var registered: Boolean = false + private var selectOnNextReload: UUID? = null + private val changeListener: StorageChangeListener = { this@UUIDMapEditor.listChanged() } + + override fun createComponent(): JComponent { + if (!isTreeInitialized) { + initTree() + isTreeInitialized = true + } + if (!registered) { + driver.theMap.addChangeListener(changeListener) + registered = true + } + return super.createComponent() + } + + override fun createActions(fromPopup: Boolean): List { + val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.shared.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) { + override fun actionPerformed(e: AnActionEvent) { + val modelList = driver.constructModelList() + val model = ZBModel(modelList) + val context = driver.createContext(model) + val popup = ZBComboBoxPopup(context, null, ::onItemSelected) + model.whenListChanged { + popup.syncWithModelChange() + } + popup.showInBestPositionFor(e.dataContext) + } + } + return listOf(add, MyDeleteAction()) + } + + override fun onItemDeleted(item: Any?) { + if (item is UUID) { + driver.theMap.remove(item) + } + super.onItemDeleted(item) + } + + private fun onItemSelected(elem: ListElem) { + if (elem !is ListElem.Pseudo) + return + zigCoroutineScope.launch(myWholePanel.asContextElement()) { + val uuid = driver.resolvePseudo(myWholePanel, elem) + if (uuid != null) { + withEDTContext(myWholePanel.asContextElement()) { + applyUUIDNowOrOnReload(uuid) + } + } + } + } + + override fun reset() { + reloadTree() + super.reset() + } + + override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.shared.list.empty") + + override fun disposeUIResources() { + super.disposeUIResources() + if (registered) { + driver.theMap.removeChangeListener(changeListener) + } + } + + private fun addElem(uuid: UUID, elem: T) { + val node = MyNode(driver.createNamedConfigurable(uuid, elem)) + addNode(node, myRoot) + } + + private fun reloadTree() { + val currentSelection = selectedObject?.asSafely() + myRoot.removeAllChildren() + val onReload = selectOnNextReload + selectOnNextReload = null + var hasOnReload = false + driver.theMap.forEach { (uuid, elem) -> + addElem(uuid, elem) + if (uuid == onReload) { + hasOnReload = true + } + } + (myTree.model as DefaultTreeModel).reload() + if (hasOnReload) { + selectNodeInTree(onReload) + return + } + currentSelection?.let { + selectNodeInTree(it) + } + } + + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + selectNodeInTree(uuid) + val currentSelection = selectedObject?.asSafely() + if (uuid != null && uuid != currentSelection) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null + } + } + + private suspend fun listChanged() { + withEDTContext(myWholePanel.asContextElement()) { + reloadTree() + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt new file mode 100644 index 00000000..b77b94b1 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/UUIDMapSelector.kt @@ -0,0 +1,161 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.StorageChangeListener +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.EDT +import com.intellij.openapi.observable.util.whenListChanged +import com.intellij.openapi.options.ShowSettingsUtil +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Row +import com.intellij.util.concurrency.annotations.RequiresEdt +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.awt.event.ItemEvent +import java.util.UUID +import javax.swing.JButton + +abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable { + private val comboBox: ZBComboBox + private var selectOnNextReload: UUID? = null + private val model: ZBModel + private var editButton: JButton? = null + private val changeListener: StorageChangeListener = { this@UUIDMapSelector.listChanged() } + init { + model = ZBModel(driver.constructModelList()) + comboBox = driver.createComboBox(model) + comboBox.addItemListener(::itemStateChanged) + driver.theMap.addChangeListener(changeListener) + model.whenListChanged { + if (comboBox.isPopupVisible) { + comboBox.isPopupVisible = false + comboBox.isPopupVisible = true + } + } + } + + protected var selectedUUID: UUID? + get() = comboBox.selectedUUID + set(value) { + comboBox.selectedUUID = value + refreshButtonState(value) + } + + private fun refreshButtonState(item: Any?) { + editButton?.isEnabled = item is ListElem.One.Actual<*> + editButton?.repaint() + } + + private fun itemStateChanged(event: ItemEvent) { + if (event.stateChange != ItemEvent.SELECTED) { + return + } + val item = event.item + refreshButtonState(item) + if (item !is ListElem.Pseudo<*>) + return + @Suppress("UNCHECKED_CAST") + item as ListElem.Pseudo + zigCoroutineScope.launch(comboBox.asContextElement()) { + val uuid = runCatching { driver.resolvePseudo(comboBox, item) }.getOrNull() + delay(100) + withEDTContext(comboBox.asContextElement()) { + applyUUIDNowOrOnReload(uuid) + } + } + } + + protected suspend fun listChanged() { + withContext(Dispatchers.EDT + comboBox.asContextElement()) { + val list = driver.constructModelList() + model.updateContents(list) + val onReload = selectOnNextReload + selectOnNextReload = null + if (onReload != null) { + val element = list.firstOrNull { when(it) { + is ListElem.One.Actual<*> -> it.uuid == onReload + else -> false + } } + model.selectedItem = element + return@withContext + } + val selected = model.selected + if (selected != null && list.contains(selected)) { + model.selectedItem = selected + return@withContext + } + if (selected is ListElem.One.Actual<*>) { + val uuid = selected.uuid + val element = list.firstOrNull { when(it) { + is ListElem.One.Actual -> it.uuid == uuid + else -> false + } } + model.selectedItem = element + return@withContext + } + model.selectedItem = ListElem.None() + } + } + + protected fun attachComboBoxRow(row: Row): Unit = with(row) { + cell(comboBox).resizableColumn().align(AlignX.FILL) + button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e -> + zigCoroutineScope.launchWithEDT(comboBox.asContextElement()) { + var selectedUUID = comboBox.selectedUUID ?: return@launchWithEDT + val elem = driver.theMap[selectedUUID] ?: return@launchWithEDT + val config = driver.createNamedConfigurable(selectedUUID, elem) + val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(comboBox)?.contentPane, config) + if (apply) { + applyUUIDNowOrOnReload(selectedUUID) + } + } + }.component.let { + editButton = it + refreshButtonState(comboBox.selectedItem) + } + } + + @RequiresEdt + private fun applyUUIDNowOrOnReload(uuid: UUID?) { + comboBox.selectedUUID = uuid + if (uuid != null && comboBox.selectedUUID == null) { + selectOnNextReload = uuid + } else { + selectOnNextReload = null + } + } + + override fun dispose() { + zigToolchainList.removeChangeListener(changeListener) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt new file mode 100644 index 00000000..cd59bd9b --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt @@ -0,0 +1,82 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.UUID + + +sealed interface ListElemIn +@Suppress("UNCHECKED_CAST") +sealed interface ListElem : ListElemIn { + sealed interface Pseudo: ListElem + sealed interface One : ListElem { + val instance: T + + @JvmRecord + data class Suggested(override val instance: T): One, Pseudo + + @JvmRecord + data class Actual(val uuid: UUID, override val instance: T): One + } + class None private constructor(): ListElem { + companion object { + private val INSTANCE = None() + operator fun invoke(): None { + return INSTANCE as None + } + } + } + class Download private constructor(): ListElem, Pseudo { + companion object { + private val INSTANCE = Download() + operator fun invoke(): Download { + return INSTANCE as Download + } + } + } + class FromDisk private constructor(): ListElem, Pseudo { + companion object { + private val INSTANCE = FromDisk() + operator fun invoke(): FromDisk { + return INSTANCE as FromDisk + } + } + } + data class Pending(val elems: Flow>): ListElem + + companion object { + private val fetchGroup = listOf>(Download(), FromDisk()) + fun fetchGroup() = fetchGroup as List> + } +} + +@JvmRecord +data class Separator(val text: String, val line: Boolean) : ListElemIn + +fun Pair.asActual() = ListElem.One.Actual(first, second) + +fun T.asSuggested() = ListElem.One.Suggested(this) + +fun Flow.asPending() = ListElem.Pending(map { it.asSuggested() }) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt new file mode 100644 index 00000000..dbb8e82c --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/model.kt @@ -0,0 +1,255 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.ui + +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.asContextElement +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.ui.CellRendererPanel +import com.intellij.ui.CollectionComboBoxModel +import com.intellij.ui.ColoredListCellRenderer +import com.intellij.ui.GroupHeaderSeparator +import com.intellij.ui.SimpleColoredComponent +import com.intellij.ui.components.panels.OpaquePanel +import com.intellij.ui.popup.list.ComboBoxPopup +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.EmptyIcon +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.awt.BorderLayout +import java.awt.Component +import java.util.IdentityHashMap +import java.util.UUID +import java.util.function.Consumer +import javax.accessibility.AccessibleContext +import javax.swing.JList +import javax.swing.border.Border + +class ZBComboBoxPopup( + context: ZBContext, + selected: ListElem?, + onItemSelected: Consumer>, +) : ComboBoxPopup>(context, selected, onItemSelected) + +open class ZBComboBox(model: ZBModel, renderer: (() -> ZBModel)-> ZBCellRenderer): ComboBox>(model) { + init { + setRenderer(renderer { model }) + } + + var selectedUUID: UUID? + set(value) { + if (value == null) { + selectedItem = ListElem.None + return + } + for (i in 0.. -> item.uuid + else -> null + } + } +} + +class ZBModel private constructor(elements: List>, private var separators: MutableMap, Separator>) : CollectionComboBoxModel>(elements) { + private var counter: Int = 0 + companion object { + operator fun invoke(input: List>): ZBModel { + val (elements, separators) = convert(input) + val model = ZBModel(elements, separators) + model.launchPendingResolve() + return model + } + + private fun convert(input: List>): Pair>, MutableMap, Separator>> { + val separators = IdentityHashMap, Separator>() + var lastSeparator: Separator? = null + val elements = ArrayList>() + input.forEach { + when (it) { + is ListElem -> { + if (lastSeparator != null) { + separators[it] = lastSeparator + lastSeparator = null + } + elements.add(it) + } + + is Separator -> lastSeparator = it + } + } + return elements to separators + } + } + + fun separatorAbove(elem: ListElem) = separators[elem] + + private fun launchPendingResolve() { + runInEdt(ModalityState.any()) { + val counter = this.counter + val size = this.size + for (i in 0.. + insertBefore(elem, newElem, counter) + } + remove(elem, counter) + } + } + } + } + + @RequiresEdt + private fun remove(old: ListElem, oldCounter: Int) { + val newCounter = this@ZBModel.counter + if (oldCounter != newCounter) { + return + } + val index = this@ZBModel.getElementIndex(old) + this@ZBModel.remove(index) + val sep = separators.remove(old) + if (sep != null && this@ZBModel.size > index) { + this@ZBModel.getElementAt(index)?.let { separators[it] = sep } + } + } + + @RequiresEdt + private fun insertBefore(old: ListElem, new: ListElem?, oldCounter: Int) { + val newCounter = this@ZBModel.counter + if (oldCounter != newCounter) { + return + } + if (new == null) { + return + } + val currentIndex = this@ZBModel.getElementIndex(old) + separators.remove(old)?.let { + separators.put(new, it) + } + this@ZBModel.add(currentIndex, new) + } + + @RequiresEdt + fun updateContents(input: List>) { + counter++ + val (elements, separators) = convert(input) + this.separators = separators + replaceAll(elements) + launchPendingResolve() + } +} + +open class ZBContext(private val project: Project?, private val model: ZBModel, private val getRenderer: (() -> ZBModel) -> ZBCellRenderer) : ComboBoxPopup.Context> { + override fun getProject(): Project? { + return project + } + + override fun getModel(): ZBModel { + return model + } + + override fun getRenderer(): ZBCellRenderer { + return getRenderer(::getModel) + } +} + +abstract class ZBCellRenderer(val getModel: () -> ZBModel) : ColoredListCellRenderer>() { + final override fun getListCellRendererComponent( + list: JList?>?, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ): Component? { + val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent + val panel = object : CellRendererPanel(BorderLayout()) { + val myContext = component.accessibleContext + + override fun getAccessibleContext(): AccessibleContext? { + return myContext + } + + override fun setBorder(border: Border?) { + component.border = border + } + } + panel.add(component, BorderLayout.CENTER) + + component.isOpaque = true + list?.let { background = if (selected) it.selectionBackground else it.background } + + val model = getModel() + + if (index == -1) { + component.isOpaque = false + panel.isOpaque = false + return panel + } + + val separator = value?.let { model.separatorAbove(it) } + + if (separator != null) { + val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3 + val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0)) + separatorComponent.isHideLine = !separator.line + separatorComponent.caption = separator.text.ifBlank { null } + val wrapper = OpaquePanel(BorderLayout()) + wrapper.add(separatorComponent, BorderLayout.CENTER) + list?.let { wrapper.background = it.background } + panel.add(wrapper, BorderLayout.NORTH) + } + + return panel + } + + abstract override fun customizeCellRenderer( + list: JList?>, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) +} + +private val EMPTY_ICON = EmptyIcon.create(1, 16) \ No newline at end of file diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index d81ebb97..3de98cc2 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -110,22 +110,22 @@ build.tool.window.status.error.general=Error while running zig build -l build.tool.window.status.no-builds=No builds currently in progress build.tool.window.status.timeout=zig build -l timed out after {0} seconds. zig=Zig +settings.shared.list.add-action.name=Add New +settings.shared.list.empty=Select an entry to view or edit its details here settings.project.display-name=Zig settings.toolchain.base.name.label=Name settings.toolchain.local.path.label=Toolchain location settings.toolchain.local.version.label=Detected zig version settings.toolchain.local.std.label=Override standard library -settings.toolchain.editor.display-name=Zig settings.toolchain.editor.toolchain.label=Toolchain settings.toolchain.editor.toolchain-default.label=Default toolchain settings.toolchain.editor.toolchain.edit-button.name=Edit settings.toolchain.model.detected.separator=Detected toolchains settings.toolchain.model.none.text= +settings.toolchain.model.loading.text=Loading\u2026 settings.toolchain.model.from-disk.text=Add Zig from disk\u2026 settings.toolchain.model.download.text=Download Zig\u2026 settings.toolchain.list.title=Toolchains -settings.toolchain.list.add-action.name=Add New -settings.toolchain.list.empty=Select a toolchain to view or edit its details here settings.toolchain.downloader.title=Install Zig settings.toolchain.downloader.version.label=Version: settings.toolchain.downloader.location.label=Location: diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt index ff8a9837..300dd6cd 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ToolchainZLSConfigProvider.kt @@ -24,20 +24,18 @@ package com.falsepattern.zigbrains.lsp import com.falsepattern.zigbrains.lsp.config.SuspendingZLSConfigProvider import com.falsepattern.zigbrains.lsp.config.ZLSConfig -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchain +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain import com.intellij.notification.Notification import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project -import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.util.io.toNioPathOrNull import kotlin.io.path.pathString class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider { override suspend fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { - val svc = project.zigProjectSettings - var state = svc.state - val toolchain = state.toolchain ?: project.suggestZigToolchain(UserDataHolderBase()) ?: return previous + val svc = ZigToolchainService.getInstance(project) + val toolchain = svc.toolchain ?: return previous val env = toolchain.zig.getEnv(project).getOrElse { throwable -> throwable.printStackTrace() @@ -65,16 +63,10 @@ class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider { ).notify(project) return previous } - var lib = if (state.overrideStdPath && state.explicitPathToStd != null) { - state.explicitPathToStd?.toNioPathOrNull() ?: run { - Notification( - "zigbrains-lsp", - "Invalid zig standard library path override: ${state.explicitPathToStd}", - NotificationType.ERROR - ).notify(project) - null - } - } else null + var lib = if (toolchain is LocalZigToolchain) + toolchain.std + else + null if (lib == null) { lib = env.libDirectory.toNioPathOrNull() ?: run { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt deleted file mode 100644 index 63228b94..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSProjectConfigurationProvider.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp - -import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsConfigurable -import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsPanel -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager - -class ZLSProjectConfigurationProvider: ZigProjectConfigurationProvider { - override fun handleMainConfigChanged(project: Project) { - startLSP(project, true) - } - - override fun createConfigurable(project: Project): SubConfigurable { - return ZLSSettingsConfigurable(project) - } - - override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel { - return ZLSSettingsPanel(ProjectManager.getInstance().defaultProject) - } - - override val priority: Int - get() = 1000 -} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt index d197dd30..7f4ccc86 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt @@ -22,36 +22,19 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity import com.intellij.ui.EditorNotifications import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlin.io.path.pathString class ZLSStartup: ProjectActivity { override suspend fun execute(project: Project) { - val zlsState = project.zlsSettings.state - if (zlsState.zlsPath.isBlank()) { - val env = if (DirenvCmd.direnvInstalled() && !project.isDefault && project.zigProjectSettings.state.direnv) - project.getDirenv() - else - emptyEnv - env.findExecutableOnPATH("zls")?.let { - zlsState.zlsPath = it.pathString - project.zlsSettings.state = zlsState - } - } project.zigCoroutineScope.launch { - var currentState = project.zlsRunningAsync() + var currentState = project.zlsRunning() while (!project.isDisposed) { - val running = project.zlsRunningAsync() + val running = project.zlsRunning() if (currentState != running) { EditorNotifications.getInstance(project).updateAllNotifications() } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt index 6470d863..5f499b0e 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt @@ -22,11 +22,8 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv import com.falsepattern.zigbrains.lsp.config.ZLSConfigProviderBase -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.project.settings.zigProjectSettings +import com.falsepattern.zigbrains.lsp.zls.ZLSService import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -55,30 +52,9 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje @OptIn(ExperimentalSerializationApi::class) suspend fun getCommand(project: Project): List? { - val svc = project.zlsSettings - val state = svc.state - val zlsPath: Path = state.zlsPath.let { zlsPath -> - if (zlsPath.isEmpty()) { - val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv - env.findExecutableOnPATH("zls") ?: run { - Notification( - "zigbrains-lsp", - ZLSBundle.message("notification.message.could-not-detect.content"), - NotificationType.ERROR - ).notify(project) - return null - } - } else { - zlsPath.toNioPathOrNull() ?: run { - Notification( - "zigbrains-lsp", - ZLSBundle.message("notification.message.zls-exe-path-invalid.content", zlsPath), - NotificationType.ERROR - ).notify(project) - return null - } - } - } + val svc = ZLSService.getInstance(project) + val zls = svc.zls ?: return null + val zlsPath: Path = zls.path if (!zlsPath.toFile().exists()) { Notification( "zigbrains-lsp", @@ -95,7 +71,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje ).notify(project) return null } - val configPath: Path? = state.zlsConfigPath.let { configPath -> + val configPath: Path? = "".let { configPath -> if (configPath.isNotBlank()) { configPath.toNioPathOrNull()?.let { nioPath -> if (!nioPath.toFile().exists()) { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt index c46e9ac7..783f611f 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.lsp.settings.zlsSettings +import com.falsepattern.zigbrains.lsp.zls.ZLSService import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -68,39 +68,29 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS } features.inlayHintFeature = object: LSPInlayHintFeature() { override fun isEnabled(file: PsiFile): Boolean { - return features.project.zlsSettings.state.inlayHints + return ZLSService.getInstance(project).zls?.settings?.inlayHints == true } } return features } - override fun isEnabled(project: Project) = project.zlsEnabledSync() + override fun isEnabled(project: Project) = project.zlsEnabled() override fun setEnabled(enabled: Boolean, project: Project) { project.zlsEnabled(enabled) } } -suspend fun Project.zlsEnabledAsync(): Boolean { - return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateAsync() -} - -fun Project.zlsEnabledSync(): Boolean { - return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateSync() +fun Project.zlsEnabled(): Boolean { + return (getUserData(ENABLED_KEY) != false) && ZLSService.getInstance(this).zls?.isValid() == true } fun Project.zlsEnabled(value: Boolean) { putUserData(ENABLED_KEY, value) } -suspend fun Project.zlsRunningAsync(): Boolean { - if (!zlsEnabledAsync()) - return false - return lsm.isRunning -} - -fun Project.zlsRunningSync(): Boolean { - if (!zlsEnabledSync()) +fun Project.zlsRunning(): Boolean { + if (!zlsEnabled()) return false return lsm.isRunning } @@ -135,7 +125,7 @@ private suspend fun doStart(project: Project, restart: Boolean) { project.lsm.stop("ZigBrains") delay(250) } - if (project.zlsSettings.validateAsync()) { + if (ZLSService.getInstance(project).zls?.isValid() == true) { delay(250) project.lsm.start("ZigBrains") } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt index a812b8b8..026e7c22 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt @@ -23,8 +23,8 @@ package com.falsepattern.zigbrains.lsp.notification import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.settings.zlsSettings -import com.falsepattern.zigbrains.lsp.zlsRunningAsync +import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.zlsRunning import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.zig.ZigFileType import com.falsepattern.zigbrains.zon.ZonFileType @@ -49,10 +49,10 @@ class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware { else -> return null } val task = project.zigCoroutineScope.async { - if (project.zlsRunningAsync()) { + if (project.zlsRunning()) { return@async null } else { - return@async project.zlsSettings.validateAsync() + return@async ZLSService.getInstance(project).zls?.isValid() == true } } return Function { editor -> diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt deleted file mode 100644 index 819a0bc2..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt +++ /dev/null @@ -1,158 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp.settings - -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv -import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.startLSP -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.intellij.ide.IdeEventQueue -import com.intellij.openapi.components.* -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.platform.ide.progress.runWithModalProgressBlocking -import com.intellij.util.application -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import java.nio.file.Path -import kotlin.io.path.isExecutable -import kotlin.io.path.isRegularFile - -@Service(Service.Level.PROJECT) -@State( - name = "ZLSSettings", - storages = [Storage(value = "zigbrains.xml")] -) -class ZLSProjectSettingsService(val project: Project): PersistentStateComponent { - @Volatile - private var state = ZLSSettings() - @Volatile - private var dirty = true - @Volatile - private var valid = false - - private val mutex = Mutex() - override fun getState(): ZLSSettings { - return state.copy() - } - - fun setState(value: ZLSSettings) { - runBlocking { - mutex.withLock { - this@ZLSProjectSettingsService.state = value - dirty = true - } - } - startLSP(project, true) - } - - override fun loadState(state: ZLSSettings) { - setState(state) - } - - suspend fun validateAsync(): Boolean { - mutex.withLock { - if (dirty) { - val state = this.state - valid = doValidate(project, state) - dirty = false - } - return valid - } - } - - fun validateSync(): Boolean { - val isValid: Boolean? = runBlocking { - mutex.withLock { - if (dirty) - null - else - valid - } - } - if (isValid != null) { - return isValid - } - return if (useModalProgress()) { - runWithModalProgressBlocking(ModalTaskOwner.project(project), ZLSBundle.message("progress.title.validate")) { - validateAsync() - } - } else { - runBlocking { - validateAsync() - } - } - } -} - -private val prohibitClass: Class<*>? = runCatching { - Class.forName("com_intellij_ide_ProhibitAWTEvents".replace('_', '.')) -}.getOrNull() - -private val postProcessors: List<*>? = runCatching { - if (prohibitClass == null) - return@runCatching null - val postProcessorsField = IdeEventQueue::class.java.getDeclaredField("postProcessors") - postProcessorsField.isAccessible = true - postProcessorsField.get(IdeEventQueue.getInstance()) as? List<*> -}.getOrNull() - -private fun useModalProgress(): Boolean { - if (!application.isDispatchThread) - return false - - if (application.isWriteAccessAllowed) - return false - - if (postProcessors == null) - return true - - return postProcessors.none { prohibitClass!!.isInstance(it) } -} - -private suspend fun doValidate(project: Project, state: ZLSSettings): Boolean { - val zlsPath: Path = state.zlsPath.let { zlsPath -> - if (zlsPath.isEmpty()) { - val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv - env.findExecutableOnPATH("zls") ?: run { - return false - } - } else { - zlsPath.toNioPathOrNull() ?: run { - return false - } - } - } - if (!zlsPath.toFile().exists()) { - return false - } - if (!zlsPath.isRegularFile() || !zlsPath.isExecutable()) { - return false - } - return true -} - -val Project.zlsSettings get() = service() \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt index d7f2f26e..75f29d74 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettings.kt @@ -25,35 +25,31 @@ package com.falsepattern.zigbrains.lsp.settings import com.falsepattern.zigbrains.lsp.config.SemanticTokens import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute import org.jetbrains.annotations.NonNls @Suppress("PropertyName") data class ZLSSettings( - var zlsPath: @NonNls String = "", - var zlsConfigPath: @NonNls String = "", - val inlayHints: Boolean = true, - val enable_snippets: Boolean = true, - val enable_argument_placeholders: Boolean = true, - val completion_label_details: Boolean = true, - val enable_build_on_save: Boolean = false, - val build_on_save_args: String = "", - val semantic_tokens: SemanticTokens = SemanticTokens.full, - val inlay_hints_show_variable_type_hints: Boolean = true, - val inlay_hints_show_struct_literal_field_type: Boolean = true, - val inlay_hints_show_parameter_name: Boolean = true, - val inlay_hints_show_builtin: Boolean = true, - val inlay_hints_exclude_single_argument: Boolean = true, - val inlay_hints_hide_redundant_param_names: Boolean = false, - val inlay_hints_hide_redundant_param_names_last_token: Boolean = false, - val warn_style: Boolean = false, - val highlight_global_var_declarations: Boolean = false, - val skip_std_references: Boolean = false, - val prefer_ast_check_as_child_process: Boolean = true, - val builtin_path: String? = null, - val build_runner_path: @NonNls String? = null, - val global_cache_path: @NonNls String? = null, -): ZigProjectConfigurationProvider.Settings { - override fun apply(project: Project) { - project.zlsSettings.loadState(this) - } -} + @JvmField @Attribute val zlsConfigPath: @NonNls String = "", + @JvmField @Attribute val inlayHints: Boolean = true, + @JvmField @Attribute val enable_snippets: Boolean = true, + @JvmField @Attribute val enable_argument_placeholders: Boolean = true, + @JvmField @Attribute val completion_label_details: Boolean = true, + @JvmField @Attribute val enable_build_on_save: Boolean = false, + @JvmField @Attribute val build_on_save_args: String = "", + @JvmField @Attribute val semantic_tokens: SemanticTokens = SemanticTokens.full, + @JvmField @Attribute val inlay_hints_show_variable_type_hints: Boolean = true, + @JvmField @Attribute val inlay_hints_show_struct_literal_field_type: Boolean = true, + @JvmField @Attribute val inlay_hints_show_parameter_name: Boolean = true, + @JvmField @Attribute val inlay_hints_show_builtin: Boolean = true, + @JvmField @Attribute val inlay_hints_exclude_single_argument: Boolean = true, + @JvmField @Attribute val inlay_hints_hide_redundant_param_names: Boolean = false, + @JvmField @Attribute val inlay_hints_hide_redundant_param_names_last_token: Boolean = false, + @JvmField @Attribute val warn_style: Boolean = false, + @JvmField @Attribute val highlight_global_var_declarations: Boolean = false, + @JvmField @Attribute val skip_std_references: Boolean = false, + @JvmField @Attribute val prefer_ast_check_as_child_process: Boolean = true, + @JvmField @Attribute val builtin_path: String? = null, + @JvmField @Attribute val build_runner_path: @NonNls String? = null, + @JvmField @Attribute val global_cache_path: @NonNls String? = null, +) \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt index b11e3ab4..43454bf7 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt @@ -24,12 +24,13 @@ package com.falsepattern.zigbrains.lsp.settings import com.falsepattern.zigbrains.lsp.config.ZLSConfig import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider +import com.falsepattern.zigbrains.lsp.zls.ZLSService import com.falsepattern.zigbrains.shared.cli.translateCommandline import com.intellij.openapi.project.Project class ZLSSettingsConfigProvider: ZLSConfigProvider { override fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { - val state = project.zlsSettings.state + val state = ZLSService.getInstance(project).zls?.settings ?: return previous return previous.copy( enable_snippets = state.enable_snippets, enable_argument_placeholders = state.enable_argument_placeholders, diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt deleted file mode 100644 index fad06b27..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp.settings - -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.shared.SubConfigurable -import com.intellij.openapi.project.Project -import com.intellij.openapi.util.Disposer -import com.intellij.ui.dsl.builder.Panel - -class ZLSSettingsConfigurable(private val project: Project): SubConfigurable { - private var appSettingsComponent: ZLSSettingsPanel? = null - override fun createComponent(holder: ZigProjectConfigurationProvider.SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel { - val settingsPanel = ZLSSettingsPanel(project).apply { attach(panel) }.also { Disposer.register(this, it) } - appSettingsComponent = settingsPanel - return settingsPanel - } - - override fun isModified(): Boolean { - val data = appSettingsComponent?.data ?: return false - return project.zlsSettings.state != data - } - - override fun apply() { - val data = appSettingsComponent?.data ?: return - val settings = project.zlsSettings - settings.state = data - } - - override fun reset() { - appSettingsComponent?.data = project.zlsSettings.state - } - - override fun dispose() { - appSettingsComponent = null - } -} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt deleted file mode 100644 index 9d28c16c..00000000 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt +++ /dev/null @@ -1,354 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.lsp.settings - -import com.falsepattern.zigbrains.direnv.DirenvCmd -import com.falsepattern.zigbrains.direnv.Env -import com.falsepattern.zigbrains.direnv.emptyEnv -import com.falsepattern.zigbrains.direnv.getDirenv -import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.config.SemanticTokens -import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider -import com.falsepattern.zigbrains.project.settings.zigProjectSettings -import com.falsepattern.zigbrains.shared.cli.call -import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.execution.processTools.mapFlat -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.guessProjectDir -import com.intellij.openapi.ui.ComboBox -import com.intellij.openapi.util.Disposer -import com.intellij.openapi.util.io.toNioPathOrNull -import com.intellij.openapi.vfs.toNioPathOrNull -import com.intellij.platform.ide.progress.ModalTaskOwner -import com.intellij.platform.ide.progress.TaskCancellation -import com.intellij.platform.ide.progress.withModalProgress -import com.intellij.ui.DocumentAdapter -import com.intellij.ui.JBColor -import com.intellij.ui.components.JBCheckBox -import com.intellij.ui.components.JBTextArea -import com.intellij.ui.components.fields.ExtendableTextField -import com.intellij.ui.components.textFieldWithBrowseButton -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.Panel -import com.intellij.ui.dsl.builder.Row -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.jetbrains.annotations.PropertyKey -import javax.swing.event.DocumentEvent -import kotlin.io.path.pathString - -@Suppress("PrivatePropertyName") -class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel { - private val zlsPath = textFieldWithBrowseButton( - project, - FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() - .withTitle(ZLSBundle.message("settings.zls-path.browse.title")), - ).also { - it.textField.document.addDocumentListener(object: DocumentAdapter() { - override fun textChanged(p0: DocumentEvent) { - dispatchUpdateUI() - } - }) - Disposer.register(this, it) - } - private val zlsConfigPath = textFieldWithBrowseButton( - project, - FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() - .withTitle(ZLSBundle.message("settings.zls-config-path.browse.title")) - ).also { Disposer.register(this, it) } - - private val zlsVersion = JBTextArea().also { it.isEditable = false } - - private var debounce: Job? = null - - private var direnv: Boolean = project.zigProjectSettings.state.direnv - - private val inlayHints = JBCheckBox() - private val enable_snippets = JBCheckBox() - private val enable_argument_placeholders = JBCheckBox() - private val completion_label_details = JBCheckBox() - private val enable_build_on_save = JBCheckBox() - private val build_on_save_args = ExtendableTextField() - private val semantic_tokens = ComboBox(SemanticTokens.entries.toTypedArray()) - private val inlay_hints_show_variable_type_hints = JBCheckBox() - private val inlay_hints_show_struct_literal_field_type = JBCheckBox() - private val inlay_hints_show_parameter_name = JBCheckBox() - private val inlay_hints_show_builtin = JBCheckBox() - private val inlay_hints_exclude_single_argument = JBCheckBox() - private val inlay_hints_hide_redundant_param_names = JBCheckBox() - private val inlay_hints_hide_redundant_param_names_last_token = JBCheckBox() - private val warn_style = JBCheckBox() - private val highlight_global_var_declarations = JBCheckBox() - private val skip_std_references = JBCheckBox() - private val prefer_ast_check_as_child_process = JBCheckBox() - private val builtin_path = ExtendableTextField() - private val build_runner_path = ExtendableTextField() - private val global_cache_path = ExtendableTextField() - - override fun attach(p: Panel) = with(p) { - if (!project.isDefault) { - group(ZLSBundle.message("settings.group.title")) { - fancyRow( - "settings.zls-path.label", - "settings.zls-path.tooltip" - ) { - cell(zlsPath).resizableColumn().align(AlignX.FILL) - } - row(ZLSBundle.message("settings.zls-version.label")) { - cell(zlsVersion) - } - fancyRow( - "settings.zls-config-path.label", - "settings.zls-config-path.tooltip" - ) { cell(zlsConfigPath).align(AlignX.FILL) } - fancyRow( - "settings.enable_snippets.label", - "settings.enable_snippets.tooltip" - ) { cell(enable_snippets) } - fancyRow( - "settings.enable_argument_placeholders.label", - "settings.enable_argument_placeholders.tooltip" - ) { cell(enable_argument_placeholders) } - fancyRow( - "settings.completion_label_details.label", - "settings.completion_label_details.tooltip" - ) { cell(completion_label_details) } - fancyRow( - "settings.enable_build_on_save.label", - "settings.enable_build_on_save.tooltip" - ) { cell(enable_build_on_save) } - fancyRow( - "settings.build_on_save_args.label", - "settings.build_on_save_args.tooltip" - ) { cell(build_on_save_args).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.semantic_tokens.label", - "settings.semantic_tokens.tooltip" - ) { cell(semantic_tokens) } - group(ZLSBundle.message("settings.inlay-hints-group.label")) { - fancyRow( - "settings.inlay-hints-enable.label", - "settings.inlay-hints-enable.tooltip" - ) { cell(inlayHints) } - fancyRow( - "settings.inlay_hints_show_variable_type_hints.label", - "settings.inlay_hints_show_variable_type_hints.tooltip" - ) { cell(inlay_hints_show_variable_type_hints) } - fancyRow( - "settings.inlay_hints_show_struct_literal_field_type.label", - "settings.inlay_hints_show_struct_literal_field_type.tooltip" - ) { cell(inlay_hints_show_struct_literal_field_type) } - fancyRow( - "settings.inlay_hints_show_parameter_name.label", - "settings.inlay_hints_show_parameter_name.tooltip" - ) { cell(inlay_hints_show_parameter_name) } - fancyRow( - "settings.inlay_hints_show_builtin.label", - "settings.inlay_hints_show_builtin.tooltip" - ) { cell(inlay_hints_show_builtin) } - fancyRow( - "settings.inlay_hints_exclude_single_argument.label", - "settings.inlay_hints_exclude_single_argument.tooltip" - ) { cell(inlay_hints_exclude_single_argument) } - fancyRow( - "settings.inlay_hints_hide_redundant_param_names.label", - "settings.inlay_hints_hide_redundant_param_names.tooltip" - ) { cell(inlay_hints_hide_redundant_param_names) } - fancyRow( - "settings.inlay_hints_hide_redundant_param_names_last_token.label", - "settings.inlay_hints_hide_redundant_param_names_last_token.tooltip" - ) { cell(inlay_hints_hide_redundant_param_names_last_token) } - } - fancyRow( - "settings.warn_style.label", - "settings.warn_style.tooltip" - ) { cell(warn_style) } - fancyRow( - "settings.highlight_global_var_declarations.label", - "settings.highlight_global_var_declarations.tooltip" - ) { cell(highlight_global_var_declarations) } - fancyRow( - "settings.skip_std_references.label", - "settings.skip_std_references.tooltip" - ) { cell(skip_std_references) } - fancyRow( - "settings.prefer_ast_check_as_child_process.label", - "settings.prefer_ast_check_as_child_process.tooltip" - ) { cell(prefer_ast_check_as_child_process) } - fancyRow( - "settings.builtin_path.label", - "settings.builtin_path.tooltip" - ) { cell(builtin_path).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.build_runner_path.label", - "settings.build_runner_path.tooltip" - ) { cell(build_runner_path).resizableColumn().align(AlignX.FILL) } - fancyRow( - "settings.global_cache_path.label", - "settings.global_cache_path.tooltip" - ) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) } - } - } - dispatchAutodetect(false) - } - - override fun direnvChanged(state: Boolean) { - direnv = state - dispatchAutodetect(true) - } - - override var data - get() = if (project.isDefault) ZLSSettings() else ZLSSettings( - zlsPath.text, - zlsConfigPath.text, - inlayHints.isSelected, - enable_snippets.isSelected, - enable_argument_placeholders.isSelected, - completion_label_details.isSelected, - enable_build_on_save.isSelected, - build_on_save_args.text, - semantic_tokens.item ?: SemanticTokens.full, - inlay_hints_show_variable_type_hints.isSelected, - inlay_hints_show_struct_literal_field_type.isSelected, - inlay_hints_show_parameter_name.isSelected, - inlay_hints_show_builtin.isSelected, - inlay_hints_exclude_single_argument.isSelected, - inlay_hints_hide_redundant_param_names.isSelected, - inlay_hints_hide_redundant_param_names_last_token.isSelected, - warn_style.isSelected, - highlight_global_var_declarations.isSelected, - skip_std_references.isSelected, - prefer_ast_check_as_child_process.isSelected, - builtin_path.text?.ifBlank { null }, - build_runner_path.text?.ifBlank { null }, - global_cache_path.text?.ifBlank { null }, - ) - set(value) { - zlsPath.text = value.zlsPath - zlsConfigPath.text = value.zlsConfigPath - inlayHints.isSelected = value.inlayHints - enable_snippets.isSelected = value.enable_snippets - enable_argument_placeholders.isSelected = value.enable_argument_placeholders - completion_label_details.isSelected = value.completion_label_details - enable_build_on_save.isSelected = value.enable_build_on_save - build_on_save_args.text = value.build_on_save_args - semantic_tokens.item = value.semantic_tokens - inlay_hints_show_variable_type_hints.isSelected = value.inlay_hints_show_variable_type_hints - inlay_hints_show_struct_literal_field_type.isSelected = value.inlay_hints_show_struct_literal_field_type - inlay_hints_show_parameter_name.isSelected = value.inlay_hints_show_parameter_name - inlay_hints_show_builtin.isSelected = value.inlay_hints_show_builtin - inlay_hints_exclude_single_argument.isSelected = value.inlay_hints_exclude_single_argument - inlay_hints_hide_redundant_param_names.isSelected = value.inlay_hints_hide_redundant_param_names - inlay_hints_hide_redundant_param_names_last_token.isSelected = - value.inlay_hints_hide_redundant_param_names_last_token - warn_style.isSelected = value.warn_style - highlight_global_var_declarations.isSelected = value.highlight_global_var_declarations - skip_std_references.isSelected = value.skip_std_references - prefer_ast_check_as_child_process.isSelected = value.prefer_ast_check_as_child_process - builtin_path.text = value.builtin_path ?: "" - build_runner_path.text = value.build_runner_path ?: "" - global_cache_path.text = value.global_cache_path ?: "" - dispatchUpdateUI() - } - - private fun dispatchAutodetect(force: Boolean) { - project.zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) { - withModalProgress(ModalTaskOwner.component(zlsPath), "Detecting ZLS...", TaskCancellation.cancellable()) { - autodetect(force) - } - } - } - - suspend fun autodetect(force: Boolean) { - if (force || zlsPath.text.isBlank()) { - getDirenv().findExecutableOnPATH("zls")?.let { - if (force || zlsPath.text.isBlank()) { - zlsPath.text = it.pathString - dispatchUpdateUI() - } - } - } - } - - override fun dispose() { - debounce?.cancel("Disposed") - } - - private suspend fun getDirenv(): Env { - if (!project.isDefault && DirenvCmd.direnvInstalled() && direnv) - return project.getDirenv() - return emptyEnv - } - - private fun dispatchUpdateUI() { - debounce?.cancel("New debounce") - debounce = project.zigCoroutineScope.launch { - updateUI() - } - } - - private suspend fun updateUI() { - if (project.isDefault) - return - delay(200) - val zlsPath = this.zlsPath.text.ifBlank { null }?.toNioPathOrNull() - if (zlsPath == null) { - withEDTContext(ModalityState.any()) { - zlsVersion.text = "[zls path empty or invalid]" - } - return - } - val workingDir = project.guessProjectDir()?.toNioPathOrNull() - val result = createCommandLineSafe(workingDir, zlsPath, "version") - .map { it.withEnvironment(getDirenv().env) } - .mapFlat { it.call() } - .getOrElse { throwable -> - throwable.printStackTrace() - withEDTContext(ModalityState.any()) { - zlsVersion.text = "[failed to run \"zls version\"]\n${throwable.message}" - } - return - } - val version = result.stdout.trim() - withEDTContext(ModalityState.any()) { - zlsVersion.text = version - zlsVersion.foreground = JBColor.foreground() - } - } -} - -private fun Panel.fancyRow( - label: @PropertyKey(resourceBundle = "zigbrains.lsp.Bundle") String, - tooltip: @PropertyKey(resourceBundle = "zigbrains.lsp.Bundle") String, - cb: Row.() -> Unit -) = row(ZLSBundle.message(label)) { - contextHelp(ZLSBundle.message(tooltip)) - cb() -} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt new file mode 100644 index 00000000..b38a3287 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSConfigurable.kt @@ -0,0 +1,85 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.NlsContexts +import com.intellij.openapi.util.NlsSafe +import com.intellij.ui.dsl.builder.panel +import java.util.UUID +import javax.swing.JComponent + +class ZLSConfigurable(val uuid: UUID, zls: ZLSVersion): NamedConfigurable() { + var zls: ZLSVersion = zls + set(value) { + zlsInstallations[uuid] = value + field = value + } + private var myView: ZLSPanel? = null + + override fun setDisplayName(name: String?) { + zls = zls.copy(name = name) + } + + override fun getEditableObject(): UUID? { + return uuid + } + + override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? { + return displayName + } + + override fun createOptionsPanel(): JComponent? { + var view = myView + if (view == null) { + view = ZLSPanel() + view.reset(zls) + myView = view + } + return panel { + view.attach(this@panel) + }.withMaximumWidth(20) + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return zls.name + } + + override fun isModified(): Boolean { + return myView?.isModified(zls) == true + } + + override fun apply() { + myView?.apply(zls)?.let { zls = it } + } + + override fun reset() { + myView?.reset(zls) + } + + override fun disposeUIResources() { + myView?.dispose() + myView = null + super.disposeUIResources() + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt new file mode 100644 index 00000000..70a13e52 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSInstallationsService.kt @@ -0,0 +1,51 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.falsepattern.zigbrains.lsp.zls.ZLSInstallationsService.MyState +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.falsepattern.zigbrains.shared.UUIDStorage +import com.intellij.openapi.components.* + +@Service(Service.Level.APP) +@State( + name = "ZLSInstallations", + storages = [Storage("zigbrains.xml")] +) +class ZLSInstallationsService: UUIDMapSerializable.Converting(MyState()) { + override fun serialize(value: ZLSVersion) = value.toRef() + override fun deserialize(value: ZLSVersion.Ref) = value.resolve() + override fun getStorage(state: MyState) = state.zlsInstallations + override fun updateStorage(state: MyState, storage: ZLSStorage) = state.copy(zlsInstallations = storage) + + data class MyState(@JvmField val zlsInstallations: ZLSStorage = emptyMap()) + + companion object { + @JvmStatic + fun getInstance(): ZLSInstallationsService = service() + } +} + +inline val zlsInstallations: ZLSInstallationsService get() = ZLSInstallationsService.getInstance() + +private typealias ZLSStorage = UUIDStorage \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt new file mode 100644 index 00000000..2f93225b --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSPanel.kt @@ -0,0 +1,131 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase +import com.falsepattern.zigbrains.shared.cli.call +import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.JBColor +import com.intellij.ui.components.JBTextArea +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Panel +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString + +class ZLSPanel() : ImmutableNamedElementPanelBase() { + private val pathToZLS = textFieldWithBrowseButton( + null, + FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("Path to the zls executable") + ).also { + it.textField.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + dispatchUpdateUI() + } + }) + Disposer.register(this, it) + } + private val zlsVersion = JBTextArea().also { it.isEditable = false } + private var debounce: Job? = null + + override fun attach(p: Panel): Unit = with(p) { + super.attach(p) + row("Path:") { + cell(pathToZLS).resizableColumn().align(AlignX.FILL) + } + row("Version:") { + cell(zlsVersion) + } + } + + override fun isModified(version: ZLSVersion): Boolean { + val name = nameFieldValue ?: return false + val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return false + return name != version.name || version.path != path + } + + override fun apply(version: ZLSVersion): ZLSVersion? { + val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return null + return version.copy(path = path, name = nameFieldValue ?: "") + } + + override fun reset(version: ZLSVersion) { + nameFieldValue = version.name + this.pathToZLS.text = version.path.pathString + dispatchUpdateUI() + } + + private fun dispatchUpdateUI() { + debounce?.cancel("New debounce") + debounce = zigCoroutineScope.launch { + updateUI() + } + } + + private suspend fun updateUI() { + delay(200) + val pathToZLS = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() + if (pathToZLS == null) { + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[zls path empty or invalid]" + } + return + } + val versionCommand = createCommandLineSafe(null, pathToZLS, "--version").getOrElse { + it.printStackTrace() + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[could not create \"zls --version\" command]\n${it.message}" + } + return + } + val result = versionCommand.call().getOrElse { + it.printStackTrace() + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[failed to run \"zls --version\"]\n${it.message}" + } + return + } + val version = result.stdout.trim() + + withEDTContext(ModalityState.any()) { + zlsVersion.text = version + zlsVersion.foreground = JBColor.foreground() + } + } + + override fun dispose() { + debounce?.cancel("Disposed") + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt new file mode 100644 index 00000000..500e92f4 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt @@ -0,0 +1,63 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.intellij.openapi.components.* +import com.intellij.openapi.project.Project +import com.intellij.util.xmlb.annotations.Attribute +import java.util.UUID + +@Service(Service.Level.PROJECT) +@State( + name = "ZLS", + storages = [Storage("zigbrains.xml")] +) +class ZLSService: SerializablePersistentStateComponent(MyState()) { + var zlsUUID: UUID? + get() = state.zls.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { + if (it in zigToolchainList) { + true + } else { + updateState { + it.copy(zls = "") + } + false + } + } + set(value) { + updateState { + it.copy(zls = value?.toString() ?: "") + } + } + + val zls: ZLSVersion? + get() = zlsUUID?.let { zlsInstallations[it] } + + data class MyState(@JvmField @Attribute var zls: String = "") + + companion object { + @JvmStatic + fun getInstance(project: Project): ZLSService = project.service() + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt new file mode 100644 index 00000000..2778c77a --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSVersion.kt @@ -0,0 +1,65 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls + +import com.falsepattern.zigbrains.lsp.settings.ZLSSettings +import com.falsepattern.zigbrains.shared.NamedObject +import com.intellij.openapi.util.io.toNioPathOrNull +import java.nio.file.Path +import com.intellij.util.xmlb.annotations.Attribute +import kotlin.io.path.isExecutable +import kotlin.io.path.isRegularFile +import kotlin.io.path.pathString + +data class ZLSVersion(val path: Path, override val name: String?, val settings: ZLSSettings): NamedObject { + override fun withName(newName: String?): ZLSVersion { + return copy(name = newName) + } + + fun toRef(): Ref { + return Ref(path.pathString, name, settings) + } + + fun isValid(): Boolean { + if (!path.toFile().exists()) + return false + if (!path.isRegularFile() || !path.isExecutable()) + return false + return true + } + + data class Ref( + @JvmField + @Attribute + val path: String? = "", + @JvmField + @Attribute + val name: String? = "", + @JvmField + val settings: ZLSSettings = ZLSSettings() + ) { + fun resolve(): ZLSVersion? { + return path?.ifBlank { null }?.toNioPathOrNull()?.let { ZLSVersion(it, name, settings) } + } + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt new file mode 100644 index 00000000..ce6d5ec8 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSDriver.kt @@ -0,0 +1,67 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.ui + +import com.falsepattern.zigbrains.lsp.zls.ZLSConfigurable +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.lsp.zls.zlsInstallations +import com.falsepattern.zigbrains.shared.UUIDMapSerializable +import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ListElemIn +import com.falsepattern.zigbrains.shared.ui.UUIDComboBoxDriver +import com.falsepattern.zigbrains.shared.ui.ZBComboBox +import com.falsepattern.zigbrains.shared.ui.ZBContext +import com.falsepattern.zigbrains.shared.ui.ZBModel +import com.intellij.openapi.ui.NamedConfigurable +import java.awt.Component +import java.util.UUID + +object ZLSDriver: UUIDComboBoxDriver { + override val theMap: UUIDMapSerializable.Converting + get() = zlsInstallations + + override fun constructModelList(): List> { + return ListElem.fetchGroup() + } + + override fun createContext(model: ZBModel): ZBContext { + return ZLSContext(null, model) + } + + override fun createComboBox(model: ZBModel): ZBComboBox { + return ZLSComboBox(model) + } + + override suspend fun resolvePseudo( + context: Component, + elem: ListElem.Pseudo + ): UUID? { + //TODO + return null + } + + override fun createNamedConfigurable(uuid: UUID, elem: ZLSVersion): NamedConfigurable { + //TODO + return ZLSConfigurable(uuid, elem) + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt new file mode 100644 index 00000000..f93d1b00 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSEditor.kt @@ -0,0 +1,89 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.ui + +import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.shared.SubConfigurable +import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager +import com.intellij.openapi.util.Key +import com.intellij.ui.dsl.builder.Panel +import kotlinx.coroutines.launch + +class ZLSEditor(private var project: Project?, + private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): + UUIDMapSelector(ZLSDriver), + SubConfigurable, + ZigProjectConfigurationProvider.UserDataListener +{ + init { + sharedState.addUserDataChangeListener(this) + } + + override fun onUserDataChanged(key: Key<*>) { + zigCoroutineScope.launch { listChanged() } + } + + override fun attach(panel: Panel): Unit = with(panel) { + row("ZLS") { + attachComboBoxRow(this) + } + } + + override fun isModified(context: Project): Boolean { + return ZLSService.getInstance(context).zlsUUID != selectedUUID + } + + override fun apply(context: Project) { + ZLSService.getInstance(context).zlsUUID = selectedUUID + } + + override fun reset(context: Project?) { + val project = context ?: ProjectManager.getInstance().defaultProject + selectedUUID = ZLSService.getInstance(project).zlsUUID + } + + override fun dispose() { + super.dispose() + sharedState.removeUserDataChangeListener(this) + } + + override val newProjectBeforeInitSelector: Boolean get() = true + class Provider: ZigProjectConfigurationProvider { + override fun create( + project: Project?, + sharedState: ZigProjectConfigurationProvider.IUserDataBridge + ): SubConfigurable? { + return ZLSEditor(project, sharedState) + } + + override val index: Int + get() = 50 + + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt new file mode 100644 index 00000000..3ee4e221 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt @@ -0,0 +1,39 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.ui + +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +import com.intellij.openapi.ui.MasterDetailsComponent +import com.intellij.openapi.util.NlsContexts + +class ZLSListEditor : UUIDMapEditor(ZLSDriver) { + override fun getEmptySelectionString(): String { + return ZLSBundle.message("settings.list.empty") + } + + override fun getDisplayName(): @NlsContexts.ConfigurableName String? { + return ZLSBundle.message("settings.list.title") + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt new file mode 100644 index 00000000..cde2f702 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/model.kt @@ -0,0 +1,92 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.ui + +import com.falsepattern.zigbrains.Icons +import com.falsepattern.zigbrains.lsp.ZLSBundle +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.project.toolchain.base.render +import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer +import com.falsepattern.zigbrains.shared.ui.ZBComboBox +import com.falsepattern.zigbrains.shared.ui.ZBContext +import com.falsepattern.zigbrains.shared.ui.ZBModel +import com.intellij.icons.AllIcons +import com.intellij.openapi.project.Project +import com.intellij.ui.SimpleTextAttributes +import com.intellij.ui.icons.EMPTY_ICON +import javax.swing.JList +import kotlin.io.path.pathString + + +class ZLSComboBox(model: ZBModel): ZBComboBox(model, ::ZLSCellRenderer) + +class ZLSContext(project: Project?, model: ZBModel): ZBContext(project, model, ::ZLSCellRenderer) + +class ZLSCellRenderer(getModel: () -> ZBModel): ZBCellRenderer(getModel) { + override fun customizeCellRenderer( + list: JList?>, + value: ListElem?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + icon = EMPTY_ICON + when (value) { + is ListElem.One -> { + val (icon, isSuggestion) = when(value) { + is ListElem.One.Suggested -> AllIcons.General.Information to true + is ListElem.One.Actual -> Icons.Zig to false + } + this.icon = icon + val item = value.instance + //TODO proper renderer + if (item.name != null) { + append(item.name) + append(item.path.pathString, SimpleTextAttributes.GRAYED_ATTRIBUTES) + } else { + append(item.path.pathString) + } + } + + is ListElem.Download -> { + icon = AllIcons.Actions.Download + append(ZLSBundle.message("settings.model.download.text")) + } + + is ListElem.FromDisk -> { + icon = AllIcons.General.OpenDisk + append(ZLSBundle.message("settings.model.from-disk.text")) + } + is ListElem.Pending -> { + icon = AllIcons.Empty + append(ZLSBundle.message("settings.model.loading.text"), SimpleTextAttributes.GRAYED_ATTRIBUTES) + } + is ListElem.None, null -> { + icon = AllIcons.General.BalloonError + append(ZLSBundle.message("settings.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } + +} \ No newline at end of file diff --git a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml index d4b6c582..0182a606 100644 --- a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml +++ b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml @@ -49,6 +49,13 @@ + @@ -59,7 +66,7 @@ implementation="com.falsepattern.zigbrains.lsp.ToolchainZLSConfigProvider" /> diff --git a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties index 3b76263f..1696dace 100644 --- a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties +++ b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties @@ -66,3 +66,10 @@ progress.title.validate=Validating ZLS lsp.zls.name=Zig Language Server # suppress inspection "UnusedProperty" lsp.zls.description=The Zig Language Server, via ZigBrains +settings.list.title=ZLS Instances +settings.list.empty=Select a ZLS version to view or edit its details here +settings.model.detected.separator=Detected ZLS version +settings.model.none.text= +settings.model.loading.text=Loading\u2026 +settings.model.from-disk.text=Add ZLS from disk\u2026 +settings.model.download.text=Download ZLS\u2026 \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 78171fe4..1656a8e0 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -17,6 +17,11 @@ dynamic="true" name="zlsConfigProvider" /> + Date: Fri, 11 Apr 2025 02:30:17 +0200 Subject: [PATCH 21/22] almost feature complete - ZLS needs settings GUI --- .../zigbrains/direnv/ui/DirenvEditor.kt | 5 +- .../ZigProjectConfigurationProvider.kt | 8 +- .../project/toolchain/ZigToolchainService.kt | 3 +- .../project/toolchain/base/ZigToolchain.kt | 17 +- .../base/ZigToolchainConfigurable.kt | 19 +- .../base/ZigToolchainExtensionsProvider.kt | 10 +- .../toolchain/base/ZigToolchainProvider.kt | 11 +- .../toolchain/downloader/LocalSelector.kt | 163 ----------------- .../downloader/LocalToolchainDownloader.kt | 50 ++++++ .../downloader/LocalToolchainSelector.kt | 107 ++++++++++++ .../toolchain/downloader/ZigVersionInfo.kt | 120 ++----------- .../toolchain/local/LocalZigToolchain.kt | 18 +- .../local/LocalZigToolchainConfigurable.kt | 6 +- .../local/LocalZigToolchainProvider.kt | 6 +- .../ui/ZigToolchainComboBoxHandler.kt | 8 +- .../toolchain/ui/ZigToolchainDriver.kt | 27 ++- .../toolchain/ui/ZigToolchainEditor.kt | 12 +- .../com/falsepattern/zigbrains/shared/UUID.kt | 28 +++ .../zigbrains/shared/UUIDMapSerializable.kt | 14 +- .../downloader/DirectoryState.kt | 25 ++- .../downloader/Downloader.kt | 83 ++++----- .../shared/downloader/LocalSelector.kt | 138 +++++++++++++++ .../shared/downloader/VersionInfo.kt | 164 ++++++++++++++++++ .../zigbrains/shared/ui/UUIDMapSelector.kt | 75 ++++---- .../zigbrains/shared/ui/elements.kt | 8 +- .../resources/zigbrains/Bundle.properties | 29 ++-- .../falsepattern/zigbrains/lsp/ZLSStartup.kt | 7 + .../lsp/ZLSStreamConnectionProvider.kt | 6 +- .../zigbrains/lsp/ZigLanguageServerFactory.kt | 8 +- .../ZigEditorNotificationProvider.kt | 4 +- .../lsp/settings/ZLSSettingsConfigProvider.kt | 4 +- .../zigbrains/lsp/zls/ZLSService.kt | 48 ++--- .../zigbrains/lsp/zls/ZLSVersion.kt | 32 +++- .../lsp/zls/downloader/ZLSDownloader.kt | 92 ++++++++++ .../lsp/zls/downloader/ZLSLocalSelector.kt | 64 +++++++ .../lsp/zls/downloader/ZLSVersionInfo.kt | 85 +++++++++ .../zigbrains/lsp/zls/ui/ZLSDriver.kt | 80 +++++++-- .../zigbrains/lsp/zls/ui/ZLSEditor.kt | 48 +++-- .../zigbrains/lsp/zls/ui/ZLSListEditor.kt | 2 +- .../main/resources/META-INF/zigbrains-lsp.xml | 2 +- 40 files changed, 1131 insertions(+), 505 deletions(-) delete mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUID.kt rename core/src/main/kotlin/com/falsepattern/zigbrains/{project/toolchain => shared}/downloader/DirectoryState.kt (61%) rename core/src/main/kotlin/com/falsepattern/zigbrains/{project/toolchain => shared}/downloader/Downloader.kt (62%) create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt create mode 100644 core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt create mode 100644 lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt 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 3aa15bb8..15f9ff71 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 @@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.direnv.DirenvService import com.falsepattern.zigbrains.direnv.DirenvState import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY import com.falsepattern.zigbrains.shared.SubConfigurable import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -83,8 +84,8 @@ abstract class DirenvEditor(private val sharedState: ZigProjectConfigurationP } class Provider: ZigProjectConfigurationProvider { - override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { - if (project?.isDefault != false) { + override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { + if (sharedState.getUserData(PROJECT_KEY)?.isDefault != false) { return null } DirenvService.setStateFor(sharedState, DirenvState.Auto) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt index 5a2b6698..c93d03f6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/settings/ZigProjectConfigurationProvider.kt @@ -30,13 +30,15 @@ import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolderBase interface ZigProjectConfigurationProvider { - fun create(project: Project?, sharedState: IUserDataBridge): SubConfigurable? + fun create(sharedState: IUserDataBridge): SubConfigurable? val index: Int companion object { private val EXTENSION_POINT_NAME = ExtensionPointName.create("com.falsepattern.zigbrains.projectConfigProvider") + val PROJECT_KEY: Key = Key.create("Project") fun createPanels(project: Project?): List> { val sharedState = UserDataBridge() - return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(project, sharedState) } + sharedState.putUserData(PROJECT_KEY, project) + return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(sharedState) } } } @@ -51,7 +53,7 @@ interface ZigProjectConfigurationProvider { class UserDataBridge: UserDataHolderBase(), IUserDataBridge { private val listeners = ArrayList() - override fun putUserData(key: Key, value: T?) { + override fun putUserData(key: Key, value: T?) { super.putUserData(key, value) synchronized(listeners) { listeners.forEach { listener -> diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt index 6cf457fe..9395c997 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ZigToolchainService.kt @@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.project.toolchain import com.falsepattern.zigbrains.project.stdlib.ZigSyntheticLibrary import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.shared.asUUID import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.application.EDT import com.intellij.openapi.components.SerializablePersistentStateComponent @@ -44,7 +45,7 @@ import java.util.UUID ) class ZigToolchainService(private val project: Project): SerializablePersistentStateComponent(State()) { var toolchainUUID: UUID? - get() = state.toolchain.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { + get() = state.toolchain.ifBlank { null }?.asUUID()?.takeIf { if (it in zigToolchainList) { true } else { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt index 2769d274..c0f2cb3b 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchain.kt @@ -37,12 +37,12 @@ import java.nio.file.Path interface ZigToolchain: NamedObject { val zig: ZigCompilerTool get() = ZigCompilerTool(this) - fun getUserData(key: Key): T? + val extraData: Map /** * Returned type must be the same class */ - fun withUserData(key: Key, value: T?): ZigToolchain + fun withExtraData(map: Map): ZigToolchain fun workingDirectory(project: Project? = null): Path? @@ -55,7 +55,18 @@ interface ZigToolchain: NamedObject { @Attribute val marker: String? = null, @JvmField - @MapAnnotation(surroundWithTag = false) val data: Map? = null, + @JvmField + val extraData: Map? = null, ) +} + +fun T.withExtraData(key: String, value: String?): T { + val newMap = HashMap() + newMap.putAll(extraData.filter { (theKey, _) -> theKey != key}) + if (value != null) { + newMap[key] = value + } + @Suppress("UNCHECKED_CAST") + return withExtraData(newMap) as T } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt index c39841f4..46071cf5 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainConfigurable.kt @@ -22,23 +22,34 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.intellij.openapi.project.Project import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.openapi.util.Key import com.intellij.openapi.util.NlsContexts import com.intellij.ui.dsl.builder.panel import java.util.UUID +import java.util.function.Supplier import javax.swing.JComponent abstract class ZigToolchainConfigurable( val uuid: UUID, - tc: T + tc: T, + val data: ZigProjectConfigurationProvider.IUserDataBridge? ): NamedConfigurable() { var toolchain: T = tc set(value) { zigToolchainList[uuid] = value field = value } + + init { + data?.putUserData(TOOLCHAIN_KEY, Supplier{ + myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc } + }) + } private var myViews: List> = emptyList() abstract fun createPanel(): ImmutableElementPanel @@ -48,7 +59,7 @@ abstract class ZigToolchainConfigurable( if (views.isEmpty()) { views = ArrayList>() views.add(createPanel()) - views.addAll(createZigToolchainExtensionPanels()) + views.addAll(createZigToolchainExtensionPanels(data)) views.forEach { it.reset(toolchain) } myViews = views } @@ -86,4 +97,8 @@ abstract class ZigToolchainConfigurable( myViews = emptyList() super.disposeUIResources() } + + companion object { + 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 e0be25d6..8ef0c29f 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainExtensionsProvider.kt @@ -22,18 +22,20 @@ 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") -internal interface ZigToolchainExtensionsProvider { - fun createExtensionPanel(): ImmutableElementPanel? +interface ZigToolchainExtensionsProvider { + fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel? val index: Int } -fun createZigToolchainExtensionPanels(): List> { +fun createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): List> { return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull { - it.createExtensionPanel() + it.createExtensionPanel(sharedState) } } \ 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 58667862..c9504edc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/base/ZigToolchainProvider.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.project.toolchain.base +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.project.Project @@ -46,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): ZigToolchainConfigurable<*> + fun createConfigurable(uuid: UUID, toolchain: ZigToolchain, data: ZigProjectConfigurationProvider.IUserDataBridge?): ZigToolchainConfigurable<*> suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) } @@ -55,17 +56,17 @@ fun ZigToolchain.Ref.resolve(): ZigToolchain? { val marker = this.marker ?: return null val data = this.data ?: return null val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null - return provider.deserialize(data) + return provider.deserialize(data)?.let { tc -> this.extraData?.let { extraData -> tc.withExtraData(extraData) }} } fun ZigToolchain.toRef(): ZigToolchain.Ref { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this)) + return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this), this.extraData) } -fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*> { +fun ZigToolchain.createNamedConfigurable(uuid: UUID, data: ZigProjectConfigurationProvider.IUserDataBridge?): ZigToolchainConfigurable<*> { val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() - return provider.createConfigurable(uuid, this) + return provider.createConfigurable(uuid, this, data) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt deleted file mode 100644 index 93553933..00000000 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalSelector.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * This file is part of ZigBrains. - * - * Copyright (C) 2023-2025 FalsePattern - * All Rights Reserved - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * ZigBrains is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, only version 3 of the License. - * - * ZigBrains is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with ZigBrains. If not, see . - */ - -package com.falsepattern.zigbrains.project.toolchain.downloader - -import com.falsepattern.zigbrains.Icons -import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.zigToolchainList -import com.falsepattern.zigbrains.shared.coroutine.asContextElement -import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT -import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT -import com.falsepattern.zigbrains.shared.coroutine.withEDTContext -import com.falsepattern.zigbrains.shared.withUniqueName -import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.icons.AllIcons -import com.intellij.openapi.application.ModalityState -import com.intellij.openapi.fileChooser.FileChooser -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory -import com.intellij.openapi.ui.DialogBuilder -import com.intellij.openapi.util.Disposer -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.components.JBLabel -import com.intellij.ui.components.JBTextField -import com.intellij.ui.components.textFieldWithBrowseButton -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.panel -import com.intellij.util.concurrency.annotations.RequiresEdt -import java.awt.Component -import java.util.concurrent.atomic.AtomicBoolean -import javax.swing.event.DocumentEvent -import kotlin.io.path.pathString - -object LocalSelector { - suspend fun browseFromDisk(component: Component, preSelected: LocalZigToolchain? = null): ZigToolchain? { - return withEDTContext(component.asContextElement()) { - doBrowseFromDisk(component, preSelected) - } - } - - @RequiresEdt - private suspend fun doBrowseFromDisk(component: Component, preSelected: LocalZigToolchain?): ZigToolchain? { - val dialog = DialogBuilder() - val name = JBTextField().also { it.columns = 25 } - val path = textFieldWithBrowseButton( - null, - FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")) - ) - Disposer.register(dialog, path) - lateinit var errorMessageBox: JBLabel - fun verify(tc: LocalZigToolchain?) { - var tc = tc - if (tc == null) { - errorMessageBox.icon = AllIcons.General.Error - errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid") - dialog.setOkActionEnabled(false) - } else { - val existingToolchain = zigToolchainList - .mapNotNull { it.second as? LocalZigToolchain } - .firstOrNull { it.location == tc.location } - if (existingToolchain != null) { - errorMessageBox.icon = AllIcons.General.Warning - errorMessageBox.text = existingToolchain.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } - ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") - dialog.setOkActionEnabled(true) - } else { - errorMessageBox.icon = AllIcons.General.Information - errorMessageBox.text = ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") - dialog.setOkActionEnabled(true) - } - } - if (tc != null) { - tc = zigToolchainList.withUniqueName(tc) - } - val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() - name.emptyText.text = tc?.name ?: "" - if (prevNameDefault) { - name.text = name.emptyText.text - } - } - suspend fun verify(path: String) { - val tc = runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) { - LocalZigToolchain.tryFromPathString(path) - } }.getOrNull() - verify(tc) - } - val active = AtomicBoolean(false) - path.addDocumentListener(object: DocumentAdapter() { - override fun textChanged(e: DocumentEvent) { - if (!active.get()) - return - zigCoroutineScope.launchWithEDT(ModalityState.current()) { - verify(path.text) - } - } - }) - val center = panel { - row(ZigBrainsBundle.message("settings.toolchain.local-selector.name.label")) { - cell(name).resizableColumn().align(AlignX.FILL) - } - row(ZigBrainsBundle.message("settings.toolchain.local-selector.path.label")) { - cell(path).resizableColumn().align(AlignX.FILL) - } - row { - errorMessageBox = JBLabel() - cell(errorMessageBox) - } - } - dialog.centerPanel(center) - dialog.setTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.title")) - dialog.addCancelAction() - dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.toolchain.local-selector.ok-action")) } - if (preSelected == null) { - val chosenFile = FileChooser.chooseFile( - FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")), - null, - null - ) - if (chosenFile != null) { - verify(chosenFile.path) - path.text = chosenFile.path - } - } else { - verify(preSelected) - path.text = preSelected.location.pathString - } - active.set(true) - if (!dialog.showAndGet()) { - active.set(false) - return null - } - active.set(false) - return runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) { - LocalZigToolchain.tryFromPathString(path.text)?.let { it.withName(name.text.ifBlank { null } ?: it.name) } - } }.getOrNull() - } -} \ No newline at end of file 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 new file mode 100644 index 00000000..c2ab44c9 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainDownloader.kt @@ -0,0 +1,50 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.getSuggestedLocalToolchainPath +import com.falsepattern.zigbrains.shared.downloader.Downloader +import com.falsepattern.zigbrains.shared.downloader.LocalSelector +import com.intellij.openapi.util.NlsContexts +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() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt new file mode 100644 index 00000000..a27106cc --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/LocalToolchainSelector.kt @@ -0,0 +1,107 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.project.toolchain.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain +import com.falsepattern.zigbrains.project.toolchain.zigToolchainList +import com.falsepattern.zigbrains.shared.coroutine.asContextElement +import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext +import com.falsepattern.zigbrains.shared.downloader.LocalSelector +import com.falsepattern.zigbrains.shared.withUniqueName +import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.icons.AllIcons +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.util.Disposer +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.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.annotations.RequiresEdt +import java.awt.Component +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString + +class LocalToolchainSelector(component: Component): LocalSelector(component) { + override val windowTitle: String + get() = ZigBrainsBundle.message("settings.toolchain.local-selector.title") + override val descriptor: FileChooserDescriptor + get() = FileChooserDescriptorFactory.createSingleFolderDescriptor() + .withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title")) + + override suspend fun verify(path: Path): VerifyResult { + var tc = resolve(path, null) + var result: VerifyResult + if (tc == null) { + result = VerifyResult( + null, + false, + AllIcons.General.Error, + ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid"), + ) + } else { + val existingToolchain = zigToolchainList + .mapNotNull { it.second as? LocalZigToolchain } + .firstOrNull { it.location == tc.location } + if (existingToolchain != null) { + result = VerifyResult( + null, + true, + AllIcons.General.Warning, + existingToolchain.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) } + ?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed") + ) + } else { + result = VerifyResult( + null, + true, + AllIcons.General.Information, + ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok") + ) + } + } + if (tc != null) { + tc = zigToolchainList.withUniqueName(tc) + } + return result.copy(name = tc?.name) + } + + override suspend fun resolve(path: Path, name: String?): LocalZigToolchain? { + return runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) { + LocalZigToolchain.tryFromPath(path)?.let { it.withName(name ?: it.name) } + } }.getOrNull() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt index 8b9d54b6..40cbdfbc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/ZigVersionInfo.kt @@ -24,6 +24,13 @@ package com.falsepattern.zigbrains.project.toolchain.downloader import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.shared.Unarchiver +import com.falsepattern.zigbrains.shared.downloader.VersionInfo +import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball +import com.falsepattern.zigbrains.shared.downloader.downloadTarball +import com.falsepattern.zigbrains.shared.downloader.flattenDownloadDir +import com.falsepattern.zigbrains.shared.downloader.getTarballIfCompatible +import com.falsepattern.zigbrains.shared.downloader.tempPluginDir +import com.falsepattern.zigbrains.shared.downloader.unpackTarball import com.intellij.openapi.application.PathManager import com.intellij.openapi.progress.EmptyProgressIndicator import com.intellij.openapi.progress.ProgressManager @@ -59,24 +66,13 @@ import kotlin.io.path.name @JvmRecord data class ZigVersionInfo( - val version: SemVer, - val date: String, + override val version: SemVer, + override val date: String, val docs: String, val notes: String, val src: Tarball?, - val dist: Tarball -) { - @Throws(Exception::class) - suspend fun downloadAndUnpack(into: Path) { - reportProgress { reporter -> - into.createDirectories() - val tarball = downloadTarball(dist, into, reporter) - unpackTarball(tarball, into, reporter) - tarball.delete() - flattenDownloadDir(into, reporter) - } - } - + override val dist: Tarball +): VersionInfo { companion object { @OptIn(ExperimentalSerializationApi::class) suspend fun downloadVersionList(): List { @@ -97,71 +93,6 @@ data class ZigVersionInfo( } } } - - @JvmRecord - @Serializable - data class Tarball(val tarball: String, val shasum: String, val size: Int) -} - -private suspend fun downloadTarball(dist: ZigVersionInfo.Tarball, into: Path, reporter: ProgressReporter): Path { - return withContext(Dispatchers.IO) { - val service = DownloadableFileService.getInstance() - val fileName = dist.tarball.substringAfterLast('/') - val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) - val desc = service.createFileDescription(dist.tarball, tempFile.name) - val downloader = service.createDownloader(listOf(desc), ZigBrainsBundle.message("settings.toolchain.downloader.service.tarball")) - val downloadResults = reporter.sizedStep(100) { - coroutineToIndicator { - downloader.download(into.toFile()) - } - } - if (downloadResults.isEmpty()) - throw IllegalStateException("No file downloaded") - return@withContext downloadResults[0].first.toPath() - } -} - -private suspend fun flattenDownloadDir(dir: Path, reporter: ProgressReporter) { - withContext(Dispatchers.IO) { - val contents = Files.newDirectoryStream(dir).use { it.toList() } - if (contents.size == 1 && contents[0].isDirectory()) { - val src = contents[0] - reporter.indeterminateStep { - coroutineToIndicator { - val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator() - indicator.isIndeterminate = true - indicator.text = ZigBrainsBundle.message("settings.toolchain.downloader.progress.flatten") - Files.newDirectoryStream(src).use { stream -> - stream.forEach { - indicator.text2 = it.name - it.move(dir.resolve(src.relativize(it))) - } - } - } - } - src.delete() - } - } -} - -@OptIn(ExperimentalPathApi::class) -private suspend fun unpackTarball(tarball: Path, into: Path, reporter: ProgressReporter) { - withContext(Dispatchers.IO) { - try { - reporter.indeterminateStep { - coroutineToIndicator { - Unarchiver.unarchive(tarball, into) - } - } - } catch (e: Throwable) { - tarball.delete() - val contents = Files.newDirectoryStream(into).use { it.toList() } - if (contents.size == 1 && contents[0].isDirectory()) { - contents[0].deleteRecursively() - } - throw e - } - } } private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? { @@ -175,36 +106,9 @@ private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? val date = data["date"]?.asSafely()?.content ?: "" val docs = data["docs"]?.asSafely()?.content ?: "" val notes = data["notes"]?.asSafely()?.content ?: "" - val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } + val src = data["src"]?.asSafely()?.let { Json.decodeFromJsonElement(it) } val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } ?: return null - return ZigVersionInfo(version, date, docs, notes, src, dist) } - -private fun getTarballIfCompatible(dist: String, tb: JsonElement): ZigVersionInfo.Tarball? { - if (!dist.contains('-')) - return null - val (arch, os) = dist.split('-', limit = 2) - val theArch = when (arch) { - "x86_64" -> CpuArch.X86_64 - "i386" -> CpuArch.X86 - "armv7a" -> CpuArch.ARM32 - "aarch64" -> CpuArch.ARM64 - else -> return null - } - val theOS = when (os) { - "linux" -> OS.Linux - "windows" -> OS.Windows - "macos" -> OS.macOS - "freebsd" -> OS.FreeBSD - else -> return null - } - if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) { - return null - } - return Json.decodeFromJsonElement(tb) -} - -private val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt index 70015f5f..290d46b0 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchain.kt @@ -36,15 +36,7 @@ import com.intellij.util.keyFMap.KeyFMap import java.nio.file.Path @JvmRecord -data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null, private val userData: KeyFMap = KeyFMap.EMPTY_MAP): ZigToolchain { - override fun getUserData(key: Key): T? { - return userData.get(key) - } - - override fun withUserData(key: Key, value: T?): LocalZigToolchain { - return copy(userData = if (value == null) userData.minus(key) else userData.plus(key, value)) - } - +data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null, override val extraData: Map = emptyMap()): ZigToolchain { override fun workingDirectory(project: Project?): Path? { return project?.guessProjectDir()?.toNioPathOrNull() } @@ -61,6 +53,10 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override return location.resolve(exeName) } + override fun withExtraData(map: Map): ZigToolchain { + return this.copy(extraData = map) + } + override fun withName(newName: String?): LocalZigToolchain { return this.copy(name = newName) } @@ -76,10 +72,6 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override } } - suspend fun tryFromPathString(pathStr: String?): LocalZigToolchain? { - return pathStr?.ifBlank { null }?.toNioPathOrNull()?.let { tryFromPath(it) } - } - suspend fun tryFromPath(path: Path): LocalZigToolchain? { var tc = LocalZigToolchain(path) if (!tc.zig.fileValid()) { 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 bc8e5adc..b64b3e26 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 @@ -22,13 +22,15 @@ package com.falsepattern.zigbrains.project.toolchain.local +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import java.util.UUID class LocalZigToolchainConfigurable( uuid: UUID, - toolchain: LocalZigToolchain -): ZigToolchainConfigurable(uuid, toolchain) { + toolchain: LocalZigToolchain, + data: ZigProjectConfigurationProvider.IUserDataBridge? +): ZigToolchainConfigurable(uuid, toolchain, data) { override fun createPanel() = LocalZigToolchainPanel() override fun setDisplayName(name: String?) { diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt index 14a79acc..1429b1cc 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/local/LocalZigToolchainProvider.kt @@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.project.toolchain.local import com.falsepattern.zigbrains.direnv.DirenvService import com.falsepattern.zigbrains.direnv.Env +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider @@ -84,10 +85,11 @@ class LocalZigToolchainProvider: ZigToolchainProvider { override fun createConfigurable( uuid: UUID, - toolchain: ZigToolchain + toolchain: ZigToolchain, + data: ZigProjectConfigurationProvider.IUserDataBridge? ): ZigToolchainConfigurable<*> { toolchain as LocalZigToolchain - return LocalZigToolchainConfigurable(uuid, toolchain) + return LocalZigToolchainConfigurable(uuid, toolchain, data) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt index ffc5a025..74ef2f85 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainComboBoxHandler.kt @@ -23,8 +23,8 @@ package com.falsepattern.zigbrains.project.toolchain.ui 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.downloader.LocalToolchainDownloader +import com.falsepattern.zigbrains.project.toolchain.downloader.LocalToolchainSelector import com.falsepattern.zigbrains.project.toolchain.zigToolchainList import com.falsepattern.zigbrains.shared.ui.ListElem import com.falsepattern.zigbrains.shared.withUniqueName @@ -36,7 +36,7 @@ internal object ZigToolchainComboBoxHandler { @RequiresBackgroundThread suspend fun onItemSelected(context: Component, elem: ListElem.Pseudo): UUID? = when(elem) { is ListElem.One.Suggested -> zigToolchainList.withUniqueName(elem.instance) - is ListElem.Download -> Downloader.downloadToolchain(context) - is ListElem.FromDisk -> LocalSelector.browseFromDisk(context) + is ListElem.Download -> LocalToolchainDownloader(context).download() + is ListElem.FromDisk -> LocalToolchainSelector(context).browse() }?.let { zigToolchainList.registerNew(it) } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainDriver.kt index f8188478..275a800c 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 @@ -23,6 +23,8 @@ package com.falsepattern.zigbrains.project.toolchain.ui 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.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains @@ -60,13 +62,6 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { return ZigToolchainComboBoxHandler.onItemSelected(context, elem) } - override fun createNamedConfigurable( - uuid: UUID, - elem: ZigToolchain - ): NamedConfigurable { - return elem.createNamedConfigurable(uuid) - } - object ForList: ZigToolchainDriver { override fun constructModelList(): List> { val modelList = ArrayList>() @@ -75,9 +70,16 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { modelList.add(suggestZigToolchains().asPending()) return modelList } + + override fun createNamedConfigurable( + uuid: UUID, + elem: ZigToolchain + ): NamedConfigurable { + return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge()) + } } - class ForSelector(val project: Project?, val data: UserDataHolder): ZigToolchainDriver { + class ForSelector(val data: ZigProjectConfigurationProvider.IUserDataBridge): ZigToolchainDriver { override fun constructModelList(): List> { val modelList = ArrayList>() modelList.add(ListElem.None()) @@ -85,8 +87,15 @@ sealed interface ZigToolchainDriver: UUIDComboBoxDriver { 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()) + modelList.add(suggestZigToolchains(data.getUserData(PROJECT_KEY), data).asPending()) return modelList } + + override fun createNamedConfigurable( + uuid: UUID, + elem: ZigToolchain + ): NamedConfigurable { + return elem.createNamedConfigurable(uuid, data) + } } } \ 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 12ac6894..51e405d4 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/ui/ZigToolchainEditor.kt @@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.project.toolchain.ui 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.ZigToolchain import com.falsepattern.zigbrains.shared.SubConfigurable @@ -35,9 +36,8 @@ import com.intellij.openapi.util.Key import com.intellij.ui.dsl.builder.Panel import kotlinx.coroutines.launch -class ZigToolchainEditor(private var project: Project?, - private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): - UUIDMapSelector(ZigToolchainDriver.ForSelector(project, sharedState)), +class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): + UUIDMapSelector(ZigToolchainDriver.ForSelector(sharedState)), SubConfigurable, ZigProjectConfigurationProvider.UserDataListener { @@ -52,7 +52,7 @@ class ZigToolchainEditor(private var project: Project?, override fun attach(p: Panel): Unit = with(p) { row(ZigBrainsBundle.message( - if (project?.isDefault == true) + if (sharedState.getUserData(PROJECT_KEY)?.isDefault == true) "settings.toolchain.editor.toolchain-default.label" else "settings.toolchain.editor.toolchain.label") @@ -81,8 +81,8 @@ class ZigToolchainEditor(private var project: Project?, override val newProjectBeforeInitSelector get() = true class Provider: ZigProjectConfigurationProvider { - override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { - return ZigToolchainEditor(project, sharedState).also { it.reset(project) } + override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable? { + return ZigToolchainEditor(sharedState).also { it.reset(sharedState.getUserData(PROJECT_KEY)) } } override val index: Int get() = 0 diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUID.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUID.kt new file mode 100644 index 00000000..deb3a33c --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUID.kt @@ -0,0 +1,28 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared + +import java.util.UUID + +fun String.asUUID(): UUID? = UUID.fromString(this) +fun UUID.asString(): String = toString() \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt index afc40ea4..f6118bc5 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/UUIDMapSerializable.kt @@ -58,10 +58,10 @@ abstract class UUIDMapSerializable(init: S): SerializablePersistentSt updateState { val newMap = HashMap() newMap.putAll(getStorage(it)) - var uuidStr = uuid.toString() + var uuidStr = uuid.asString() while (newMap.containsKey(uuidStr)) { uuid = UUID.randomUUID() - uuidStr = uuid.toString() + uuidStr = uuid.asString() } newMap[uuidStr] = value updateStorage(it, newMap) @@ -71,7 +71,7 @@ abstract class UUIDMapSerializable(init: S): SerializablePersistentSt } protected fun setStateUUID(uuid: UUID, value: T) { - val str = uuid.toString() + val str = uuid.asString() updateState { val newMap = HashMap() newMap.putAll(getStorage(it)) @@ -82,15 +82,15 @@ abstract class UUIDMapSerializable(init: S): SerializablePersistentSt } protected fun getStateUUID(uuid: UUID): T? { - return getStorage(state)[uuid.toString()] + return getStorage(state)[uuid.asString()] } protected fun hasStateUUID(uuid: UUID): Boolean { - return getStorage(state).containsKey(uuid.toString()) + return getStorage(state).containsKey(uuid.asString()) } protected fun removeStateUUID(uuid: UUID) { - val str = uuid.toString() + val str = uuid.asString() updateState { updateStorage(state, getStorage(state).filter { it.key != str }) } @@ -143,7 +143,7 @@ abstract class UUIDMapSerializable(init: S): SerializablePersistentSt return getStorage(state) .asSequence() .mapNotNull { - val uuid = UUID.fromString(it.key) ?: return@mapNotNull null + val uuid = it.key.asUUID() ?: return@mapNotNull null val tc = deserialize(it.value) ?: return@mapNotNull null uuid to tc }.iterator() diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/DirectoryState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt similarity index 61% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/DirectoryState.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt index 5bf1a3d1..ac6110e7 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/DirectoryState.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/DirectoryState.kt @@ -1,8 +1,29 @@ -package com.falsepattern.zigbrains.project.toolchain.downloader +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.downloader import java.nio.file.Files import java.nio.file.Path -import kotlin.contracts.ExperimentalContracts import kotlin.io.path.exists import kotlin.io.path.isDirectory diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt similarity index 62% rename from core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt rename to core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt index 5a7522e9..2b5a4a34 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/downloader/Downloader.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/Downloader.kt @@ -20,20 +20,17 @@ * along with ZigBrains. If not, see . */ -package com.falsepattern.zigbrains.project.toolchain.downloader +package com.falsepattern.zigbrains.shared.downloader import com.falsepattern.zigbrains.ZigBrainsBundle -import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain -import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain -import com.falsepattern.zigbrains.project.toolchain.local.getSuggestedLocalToolchainPath import com.falsepattern.zigbrains.shared.coroutine.asContextElement import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.intellij.icons.AllIcons -import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.observable.util.whenFocusGained import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.TaskCancellation @@ -45,45 +42,53 @@ import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Cell import com.intellij.ui.dsl.builder.panel -import com.intellij.util.asSafely import com.intellij.util.concurrency.annotations.RequiresEdt import java.awt.Component import java.nio.file.Path +import java.util.Vector import javax.swing.DefaultComboBoxModel import javax.swing.JList import javax.swing.event.DocumentEvent import kotlin.io.path.pathString -object Downloader { - suspend fun downloadToolchain(component: Component): ZigToolchain? { +abstract class Downloader(val component: Component) { + suspend fun download(): T? { val info = withModalProgress( ModalTaskOwner.component(component), - ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch"), + versionInfoFetchTitle, TaskCancellation.cancellable() ) { - ZigVersionInfo.downloadVersionList() + downloadVersionList() } + val selector = localSelector() val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) { - selectToolchain(info) + selectVersion(info, selector) } ?: return null withModalProgress( ModalTaskOwner.component(component), - ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion), + downloadProgressTitle(version), TaskCancellation.cancellable() ) { version.downloadAndUnpack(downloadPath) } - return LocalZigToolchain.tryFromPath(downloadPath)?.let { LocalSelector.browseFromDisk(component, it) } + return selector.browse(downloadPath) } + protected abstract val windowTitle: String + protected abstract val versionInfoFetchTitle: @NlsContexts.ProgressTitle String + protected abstract fun downloadProgressTitle(version: V): @NlsContexts.ProgressTitle String + protected abstract fun localSelector(): LocalSelector + protected abstract suspend fun downloadVersionList(): List + protected abstract fun getSuggestedPath(): Path? + @RequiresEdt - private fun selectToolchain(info: List): Pair? { + private fun selectVersion(info: List, selector: LocalSelector): Pair? { val dialog = DialogBuilder() - val theList = ComboBox(DefaultComboBoxModel(info.toTypedArray())) - theList.renderer = object: ColoredListCellRenderer() { + val theList = ComboBox(DefaultComboBoxModel(Vector(info))) + theList.renderer = object: ColoredListCellRenderer() { override fun customizeCellRenderer( - list: JList, - value: ZigVersionInfo?, + list: JList, + value: V?, index: Int, selected: Boolean, hasFocus: Boolean @@ -91,18 +96,14 @@ object Downloader { value?.let { append(it.version.rawVersion) } } } - val outputPath = textFieldWithBrowseButton( - null, - FileChooserDescriptorFactory.createSingleFolderDescriptor() - .withTitle(ZigBrainsBundle.message("settings.toolchain.downloader.chooser.title")) - ) + val outputPath = textFieldWithBrowseButton(null, selector.descriptor) Disposer.register(dialog, outputPath) outputPath.textField.columns = 50 lateinit var errorMessageBox: JBLabel fun onChanged() { val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() - val state = DirectoryState.determine(path) + val state = DirectoryState.Companion.determine(path) if (state.isValid()) { errorMessageBox.icon = AllIcons.General.Information dialog.setOkActionEnabled(true) @@ -111,12 +112,12 @@ object Downloader { dialog.setOkActionEnabled(false) } errorMessageBox.text = ZigBrainsBundle.message(when(state) { - DirectoryState.Invalid -> "settings.toolchain.downloader.state.invalid" - DirectoryState.NotAbsolute -> "settings.toolchain.downloader.state.not-absolute" - DirectoryState.NotDirectory -> "settings.toolchain.downloader.state.not-directory" - DirectoryState.NotEmpty -> "settings.toolchain.downloader.state.not-empty" - DirectoryState.CreateNew -> "settings.toolchain.downloader.state.create-new" - DirectoryState.Ok -> "settings.toolchain.downloader.state.ok" + DirectoryState.Invalid -> "settings.shared.downloader.state.invalid" + DirectoryState.NotAbsolute -> "settings.shared.downloader.state.not-absolute" + DirectoryState.NotDirectory -> "settings.shared.downloader.state.not-directory" + DirectoryState.NotEmpty -> "settings.shared.downloader.state.not-empty" + DirectoryState.CreateNew -> "settings.shared.downloader.state.create-new" + DirectoryState.Ok -> "settings.shared.downloader.state.ok" }) dialog.window.repaint() } @@ -129,20 +130,21 @@ object Downloader { } }) var archiveSizeCell: Cell<*>? = null - fun detect(item: ZigVersionInfo) { - outputPath.text = getSuggestedLocalToolchainPath()?.resolve(item.version.rawVersion)?.pathString ?: "" + fun detect(item: V) { + outputPath.text = getSuggestedPath()?.resolve(item.version.rawVersion)?.pathString ?: "" val size = item.dist.size val sizeMb = size / (1024f * 1024f) - archiveSizeCell?.comment?.text = ZigBrainsBundle.message("settings.toolchain.downloader.archive-size.text", "%.2fMB".format(sizeMb)) + archiveSizeCell?.comment?.text = ZigBrainsBundle.message("settings.shared.downloader.archive-size.text", "%.2fMB".format(sizeMb)) } theList.addItemListener { - detect(it.item as ZigVersionInfo) + @Suppress("UNCHECKED_CAST") + detect(it.item as V) } val center = panel { - row(ZigBrainsBundle.message("settings.toolchain.downloader.version.label")) { + row(ZigBrainsBundle.message("settings.shared.downloader.version.label")) { cell(theList).resizableColumn().align(AlignX.FILL) } - row(ZigBrainsBundle.message("settings.toolchain.downloader.location.label")) { + row(ZigBrainsBundle.message("settings.shared.downloader.location.label")) { cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") } } row { @@ -152,19 +154,18 @@ object Downloader { } detect(info[0]) dialog.centerPanel(center) - dialog.setTitle(ZigBrainsBundle.message("settings.toolchain.downloader.title")) + dialog.setTitle(windowTitle) dialog.addCancelAction() - dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.toolchain.downloader.ok-action")) } + dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.shared.downloader.ok-action")) } if (!dialog.showAndGet()) { return null } val path = outputPath.text.ifBlank { null }?.toNioPathOrNull() ?: return null - if (!DirectoryState.determine(path).isValid()) { + if (!DirectoryState.Companion.determine(path).isValid()) { return null } - val version = theList.selectedItem?.asSafely() - ?: return null + val version = theList.item ?: return null return path to version } 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 new file mode 100644 index 00000000..314754ac --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/LocalSelector.kt @@ -0,0 +1,138 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +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.icons.AllIcons +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.fileChooser.FileChooser +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.ui.DialogBuilder +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.ui.components.textFieldWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.concurrency.annotations.RequiresEdt +import java.awt.Component +import java.nio.file.Path +import java.util.concurrent.atomic.AtomicBoolean +import javax.swing.Icon +import javax.swing.event.DocumentEvent +import kotlin.io.path.pathString + +abstract class LocalSelector(val component: Component) { + suspend fun browse(preSelected: Path? = null): T? { + return withEDTContext(component.asContextElement()) { + doBrowseFromDisk(preSelected) + } + } + + abstract val windowTitle: String + abstract val descriptor: FileChooserDescriptor + protected abstract suspend fun verify(path: Path): VerifyResult + protected abstract suspend fun resolve(path: Path, name: String?): T? + + @RequiresEdt + private suspend fun doBrowseFromDisk(preSelected: Path?): T? { + val dialog = DialogBuilder() + val name = JBTextField().also { it.columns = 25 } + val path = textFieldWithBrowseButton(null, descriptor) + Disposer.register(dialog, path) + lateinit var errorMessageBox: JBLabel + suspend fun verifyAndUpdate(path: Path?) { + val result = path?.let { verify(it) } ?: VerifyResult( + "", + false, + AllIcons.General.Error, + ZigBrainsBundle.message("settings.shared.local-selector.state.invalid") + ) + val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank() + name.emptyText.text = result.name ?: "" + if (prevNameDefault) { + name.text = name.emptyText.text + } + errorMessageBox.icon = result.errorIcon + errorMessageBox.text = result.errorText + dialog.setOkActionEnabled(result.allowed) + } + val active = AtomicBoolean(false) + path.addDocumentListener(object: DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + if (!active.get()) + return + zigCoroutineScope.launchWithEDT(ModalityState.current()) { + verifyAndUpdate(path.text.ifBlank { null }?.toNioPathOrNull()) + } + } + }) + val center = panel { + row(ZigBrainsBundle.message("settings.shared.local-selector.name.label")) { + cell(name).resizableColumn().align(AlignX.FILL) + } + row(ZigBrainsBundle.message("settings.shared.local-selector.path.label")) { + cell(path).resizableColumn().align(AlignX.FILL) + } + row { + errorMessageBox = JBLabel() + cell(errorMessageBox) + } + } + dialog.centerPanel(center) + dialog.setTitle(windowTitle) + dialog.addCancelAction() + dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.shared.local-selector.ok-action")) } + if (preSelected == null) { + val chosenFile = FileChooser.chooseFile(descriptor, null, null) + if (chosenFile != null) { + verifyAndUpdate(chosenFile.toNioPath()) + path.text = chosenFile.path + } + } else { + verifyAndUpdate(preSelected) + path.text = preSelected.pathString + } + active.set(true) + if (!dialog.showAndGet()) { + active.set(false) + return null + } + active.set(false) + return path.text.ifBlank { null }?.toNioPathOrNull()?.let { resolve(it, name.text.ifBlank { null }) } + } + + @JvmRecord + data class VerifyResult( + val name: String?, + val allowed: Boolean, + val errorIcon: Icon, + val errorText: String, + ) +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt new file mode 100644 index 00000000..26c7d3dd --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/downloader/VersionInfo.kt @@ -0,0 +1,164 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.shared.downloader + +import com.falsepattern.zigbrains.ZigBrainsBundle +import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo +import com.falsepattern.zigbrains.shared.Unarchiver +import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.progress.EmptyProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.coroutineToIndicator +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.platform.util.progress.ProgressReporter +import com.intellij.platform.util.progress.reportProgress +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.io.createDirectories +import com.intellij.util.io.delete +import com.intellij.util.io.move +import com.intellij.util.system.CpuArch +import com.intellij.util.system.OS +import com.intellij.util.text.SemVer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.decodeFromJsonElement +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.deleteRecursively +import kotlin.io.path.isDirectory +import kotlin.io.path.name + +interface VersionInfo { + val version: SemVer + val date: String + val dist: Tarball + + @Throws(Exception::class) + suspend fun downloadAndUnpack(into: Path) { + reportProgress { reporter -> + into.createDirectories() + val tarball = downloadTarball(dist, into, reporter) + unpackTarball(tarball, into, reporter) + tarball.delete() + flattenDownloadDir(into, reporter) + } + } + + @JvmRecord + @Serializable + data class Tarball(val tarball: String, val shasum: String, val size: Int) +} + +suspend fun downloadTarball(dist: Tarball, into: Path, reporter: ProgressReporter): Path { + return withContext(Dispatchers.IO) { + val service = DownloadableFileService.getInstance() + val fileName = dist.tarball.substringAfterLast('/') + val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false) + val desc = service.createFileDescription(dist.tarball, tempFile.name) + val downloader = service.createDownloader(listOf(desc), ZigBrainsBundle.message("settings.toolchain.downloader.service.tarball")) + val downloadResults = reporter.sizedStep(100) { + coroutineToIndicator { + downloader.download(into.toFile()) + } + } + if (downloadResults.isEmpty()) + throw IllegalStateException("No file downloaded") + return@withContext downloadResults[0].first.toPath() + } +} + +suspend fun flattenDownloadDir(dir: Path, reporter: ProgressReporter) { + withContext(Dispatchers.IO) { + val contents = Files.newDirectoryStream(dir).use { it.toList() } + if (contents.size == 1 && contents[0].isDirectory()) { + val src = contents[0] + reporter.indeterminateStep { + coroutineToIndicator { + val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator() + indicator.isIndeterminate = true + indicator.text = ZigBrainsBundle.message("settings.toolchain.downloader.progress.flatten") + Files.newDirectoryStream(src).use { stream -> + stream.forEach { + indicator.text2 = it.name + it.move(dir.resolve(src.relativize(it))) + } + } + } + } + src.delete() + } + } +} + +@OptIn(ExperimentalPathApi::class) +suspend fun unpackTarball(tarball: Path, into: Path, reporter: ProgressReporter) { + withContext(Dispatchers.IO) { + try { + reporter.indeterminateStep { + coroutineToIndicator { + Unarchiver.unarchive(tarball, into) + } + } + } catch (e: Throwable) { + tarball.delete() + val contents = Files.newDirectoryStream(into).use { it.toList() } + if (contents.size == 1 && contents[0].isDirectory()) { + contents[0].deleteRecursively() + } + throw e + } + } +} + +fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? { + if (!dist.contains('-')) + return null + val (arch, os) = dist.split('-', limit = 2) + val theArch = when (arch) { + "x86_64" -> CpuArch.X86_64 + "i386", "x86" -> CpuArch.X86 + "armv7a" -> CpuArch.ARM32 + "aarch64" -> CpuArch.ARM64 + else -> return null + } + val theOS = when (os) { + "linux" -> OS.Linux + "windows" -> OS.Windows + "macos" -> OS.macOS + "freebsd" -> OS.FreeBSD + else -> return null + } + if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) { + return null + } + return Json.decodeFromJsonElement(tb) +} + +val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile() 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 b77b94b1..17a1acd8 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 @@ -28,9 +28,11 @@ 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.ui.ListElem import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.Disposable import com.intellij.openapi.application.EDT +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.observable.util.whenListChanged import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.ui.DialogWrapper @@ -57,6 +59,9 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable comboBox.addItemListener(::itemStateChanged) driver.theMap.addChangeListener(changeListener) model.whenListChanged { + zigCoroutineScope.launchWithEDT(comboBox.asContextElement()) { + tryReloadSelection() + } if (comboBox.isPopupVisible) { comboBox.isPopupVisible = false comboBox.isPopupVisible = true @@ -67,11 +72,12 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable protected var selectedUUID: UUID? get() = comboBox.selectedUUID set(value) { - comboBox.selectedUUID = value - refreshButtonState(value) + runInEdt { + applyUUIDNowOrOnReload(value) + } } - private fun refreshButtonState(item: Any?) { + private fun refreshButtonState(item: ListElem<*>) { editButton?.isEnabled = item is ListElem.One.Actual<*> editButton?.repaint() } @@ -81,6 +87,8 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable return } val item = event.item + if (item !is ListElem<*>) + return refreshButtonState(item) if (item !is ListElem.Pseudo<*>) return @@ -95,35 +103,45 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable } } + @RequiresEdt + private fun tryReloadSelection() { + val list = model.toList() + val onReload = selectOnNextReload + selectOnNextReload = null + if (onReload != null) { + val element = list.firstOrNull { when(it) { + is ListElem.One.Actual<*> -> it.uuid == onReload + else -> false + } } + if (element == null) { + selectOnNextReload = onReload + } else { + model.selectedItem = element + return + } + } + val selected = model.selected + if (selected != null && list.contains(selected)) { + model.selectedItem = selected + return + } + 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 + } + model.selectedItem = ListElem.None() + } + 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() + tryReloadSelection() } } @@ -141,7 +159,6 @@ abstract class UUIDMapSelector(val driver: UUIDComboBoxDriver): Disposable } }.component.let { editButton = it - refreshButtonState(comboBox.selectedItem) } } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt index cd59bd9b..f1916ed5 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ui/elements.kt @@ -67,7 +67,7 @@ sealed interface ListElem : ListElemIn { data class Pending(val elems: Flow>): ListElem companion object { - private val fetchGroup = listOf>(Download(), FromDisk()) + private val fetchGroup: List> = listOf(Download(), FromDisk()) fun fetchGroup() = fetchGroup as List> } } @@ -79,4 +79,8 @@ fun Pair.asActual() = ListElem.One.Actual(first, second) fun T.asSuggested() = ListElem.One.Suggested(this) -fun Flow.asPending() = ListElem.Pending(map { it.asSuggested() }) \ No newline at end of file +@JvmName("listElemFlowAsPending") +fun Flow>.asPending() = ListElem.Pending(this) + +fun Flow.asPending() = map { it.asSuggested() }.asPending() + diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index 3de98cc2..1b05b9f3 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -112,6 +112,20 @@ 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.shared.downloader.version.label=Version: +settings.shared.downloader.location.label=Location: +settings.shared.downloader.ok-action=Download +settings.shared.downloader.state.invalid=Invalid path +settings.shared.downloader.state.not-absolute=Must be an absolute path +settings.shared.downloader.state.not-directory=Path is not a directory +settings.shared.downloader.state.not-empty=Directory is not empty +settings.shared.downloader.state.create-new=Directory will be created +settings.shared.downloader.state.ok=Directory OK +settings.shared.downloader.archive-size.text=Archive size: {0} +settings.shared.local-selector.name.label=Name: +settings.shared.local-selector.path.label=Path: +settings.shared.local-selector.ok-action=Add +settings.shared.local-selector.state.invalid=Invalid path settings.project.display-name=Zig settings.toolchain.base.name.label=Name settings.toolchain.local.path.label=Toolchain location @@ -127,27 +141,14 @@ 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.downloader.title=Install Zig -settings.toolchain.downloader.version.label=Version: -settings.toolchain.downloader.location.label=Location: -settings.toolchain.downloader.ok-action=Download settings.toolchain.downloader.progress.fetch=Fetching zig version information settings.toolchain.downloader.progress.install=Installing Zig {0} settings.toolchain.downloader.progress.flatten=Flattening unpacked archive settings.toolchain.downloader.chooser.title=Zig Install Directory -settings.toolchain.downloader.state.invalid=Invalid path -settings.toolchain.downloader.state.not-absolute=Must be an absolute path -settings.toolchain.downloader.state.not-directory=Path is not a directory -settings.toolchain.downloader.state.not-empty=Directory is not empty -settings.toolchain.downloader.state.create-new=Directory will be created -settings.toolchain.downloader.state.ok=Directory OK -settings.toolchain.downloader.archive-size.text=Archive size: {0} settings.toolchain.downloader.service.index=Zig version information settings.toolchain.downloader.service.tarball=Zig archive settings.toolchain.local-selector.title=Select Zig From Disk -settings.toolchain.local-selector.name.label=Name: -settings.toolchain.local-selector.path.label=Path: -settings.toolchain.local-selector.ok-action=Add -settings.toolchain.local-selector.chooser.title=Existing Zig Install Directory +settings.toolchain.local-selector.chooser.title=Zig Installation Directory settings.toolchain.local-selector.state.invalid=Invalid toolchain path settings.toolchain.local-selector.state.already-exists-unnamed=Toolchain already exists settings.toolchain.local-selector.state.already-exists-named=Toolchain already exists as "{0}" diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt index 7f4ccc86..05c6aec3 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt @@ -22,6 +22,7 @@ package com.falsepattern.zigbrains.lsp +import com.falsepattern.zigbrains.lsp.zls.zls import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity @@ -33,7 +34,13 @@ class ZLSStartup: ProjectActivity { override suspend fun execute(project: Project) { project.zigCoroutineScope.launch { var currentState = project.zlsRunning() + var currentZLS = project.zls while (!project.isDisposed) { + val zls = project.zls + if (currentZLS != zls) { + startLSP(project, true) + } + currentZLS = zls val running = project.zlsRunning() if (currentState != running) { EditorNotifications.getInstance(project).updateAllNotifications() diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt index 5f499b0e..a55c3f39 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStreamConnectionProvider.kt @@ -23,7 +23,8 @@ package com.falsepattern.zigbrains.lsp import com.falsepattern.zigbrains.lsp.config.ZLSConfigProviderBase -import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.zls.zls +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.notification.Notification import com.intellij.notification.NotificationType @@ -52,8 +53,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje @OptIn(ExperimentalSerializationApi::class) suspend fun getCommand(project: Project): List? { - val svc = ZLSService.getInstance(project) - val zls = svc.zls ?: return null + val zls = project.zls ?: return null val zlsPath: Path = zls.path if (!zlsPath.toFile().exists()) { Notification( diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt index 783f611f..8cc3b328 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt @@ -22,7 +22,7 @@ package com.falsepattern.zigbrains.lsp -import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.zls.zls import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.components.service import com.intellij.openapi.project.Project @@ -68,7 +68,7 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS } features.inlayHintFeature = object: LSPInlayHintFeature() { override fun isEnabled(file: PsiFile): Boolean { - return ZLSService.getInstance(project).zls?.settings?.inlayHints == true + return project.zls?.settings?.inlayHints == true } } return features @@ -82,7 +82,7 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS } fun Project.zlsEnabled(): Boolean { - return (getUserData(ENABLED_KEY) != false) && ZLSService.getInstance(this).zls?.isValid() == true + return (getUserData(ENABLED_KEY) != false) && zls?.isValid() == true } fun Project.zlsEnabled(value: Boolean) { @@ -125,7 +125,7 @@ private suspend fun doStart(project: Project, restart: Boolean) { project.lsm.stop("ZigBrains") delay(250) } - if (ZLSService.getInstance(project).zls?.isValid() == true) { + if (project.zls?.isValid() == true) { delay(250) project.lsm.start("ZigBrains") } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt index 026e7c22..599ecacb 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt @@ -23,7 +23,7 @@ package com.falsepattern.zigbrains.lsp.notification import com.falsepattern.zigbrains.lsp.ZLSBundle -import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.zls.zls import com.falsepattern.zigbrains.lsp.zlsRunning import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.zig.ZigFileType @@ -52,7 +52,7 @@ class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware { if (project.zlsRunning()) { return@async null } else { - return@async ZLSService.getInstance(project).zls?.isValid() == true + return@async project.zls?.isValid() == true } } return Function { editor -> diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt index 43454bf7..aa1aaabb 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigProvider.kt @@ -24,13 +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.lsp.zls.zls 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 = ZLSService.getInstance(project).zls?.settings ?: return previous + val state = project.zls?.settings ?: return previous return previous.copy( enable_snippets = state.enable_snippets, enable_argument_placeholders = state.enable_argument_placeholders, diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt index 500e92f4..85a588a0 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ZLSService.kt @@ -22,42 +22,24 @@ package com.falsepattern.zigbrains.lsp.zls -import com.falsepattern.zigbrains.project.toolchain.zigToolchainList -import com.intellij.openapi.components.* +import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.withExtraData +import com.falsepattern.zigbrains.shared.asString +import com.falsepattern.zigbrains.shared.asUUID import com.intellij.openapi.project.Project -import com.intellij.util.xmlb.annotations.Attribute import java.util.UUID -@Service(Service.Level.PROJECT) -@State( - name = "ZLS", - storages = [Storage("zigbrains.xml")] -) -class ZLSService: SerializablePersistentStateComponent(MyState()) { - var zlsUUID: UUID? - get() = state.zls.ifBlank { null }?.let { UUID.fromString(it) }?.takeIf { - if (it in zigToolchainList) { - true - } else { - updateState { - it.copy(zls = "") - } - false - } - } - set(value) { - updateState { - it.copy(zls = value?.toString() ?: "") - } - } +fun T.withZLS(uuid: UUID?): T { + return withExtraData("zls_uuid", uuid?.asString()) +} - val zls: ZLSVersion? - get() = zlsUUID?.let { zlsInstallations[it] } +val ZigToolchain.zlsUUID: UUID? get() { + return extraData["zls_uuid"]?.asUUID() +} - data class MyState(@JvmField @Attribute var zls: String = "") +val ZigToolchain.zls: ZLSVersion? get() { + return zlsUUID?.let { zlsInstallations[it] } +} - companion object { - @JvmStatic - fun getInstance(project: Project): ZLSService = project.service() - } -} \ No newline at end of file +val Project.zls: ZLSVersion? get() = ZigToolchainService.getInstance(this).toolchain?.zls 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 2778c77a..c1de43e8 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 @@ -24,14 +24,19 @@ package com.falsepattern.zigbrains.lsp.zls import com.falsepattern.zigbrains.lsp.settings.ZLSSettings import com.falsepattern.zigbrains.shared.NamedObject +import com.falsepattern.zigbrains.shared.cli.call +import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe +import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.util.text.SemVer import java.nio.file.Path import com.intellij.util.xmlb.annotations.Attribute +import kotlin.io.path.isDirectory 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 { +data class ZLSVersion(val path: Path, override val name: String? = null, val settings: ZLSSettings = ZLSSettings()): NamedObject { override fun withName(newName: String?): ZLSVersion { return copy(name = newName) } @@ -48,6 +53,31 @@ data class ZLSVersion(val path: Path, override val name: String?, val settings: return true } + suspend fun version(): SemVer? { + if (!isValid()) + return null + val cli = createCommandLineSafe(null, path, "--version").getOrElse { return null } + val info = cli.call(5000).getOrElse { return null } + return SemVer.parseFromText(info.stdout.trim()) + } + + 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 + val version = zls.version()?.rawVersion + if (version != null) { + zls = zls.copy(name = "ZLS $version") + } + return zls + } + } + data class Ref( @JvmField @Attribute 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 new file mode 100644 index 00000000..97b74ca4 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSDownloader.kt @@ -0,0 +1,92 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.downloader + +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +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) + } + + override suspend fun downloadVersionList(): List { + val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get() ?: return emptyList() + 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 +} \ 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 new file mode 100644 index 00000000..5586bc79 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSLocalSelector.kt @@ -0,0 +1,64 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.downloader + +import com.falsepattern.zigbrains.lsp.zls.ZLSVersion +import com.falsepattern.zigbrains.lsp.zls.zlsInstallations +import com.falsepattern.zigbrains.shared.downloader.LocalSelector +import com.falsepattern.zigbrains.shared.withUniqueName +import com.intellij.icons.AllIcons +import com.intellij.openapi.fileChooser.FileChooserDescriptor +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import java.awt.Component +import java.nio.file.Path + +class ZLSLocalSelector(component: Component) : LocalSelector(component) { + override val windowTitle: String + get() = "Select ZLS from disk" + override val descriptor: FileChooserDescriptor + get() = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("ZLS binary") + + override suspend fun verify(path: Path): VerifyResult { + var zls = resolve(path, null) + var result: VerifyResult + result = if (zls == null) VerifyResult( + null, + false, + AllIcons.General.Error, + "Invalid ZLS path", + ) else VerifyResult( + null, + true, + AllIcons.General.Information, + "ZLS path OK" + ) + if (zls != null) { + zls = zlsInstallations.withUniqueName(zls) + } + return result.copy(name = zls?.name) + } + + override suspend fun resolve(path: Path, name: String?): ZLSVersion? { + return ZLSVersion.tryFromPath(path)?.let { zls -> name?.let { zls.copy(name = it) } ?: zls } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..1cc03960 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/downloader/ZLSVersionInfo.kt @@ -0,0 +1,85 @@ +/* + * This file is part of ZigBrains. + * + * Copyright (C) 2023-2025 FalsePattern + * All Rights Reserved + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * ZigBrains is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, only version 3 of the License. + * + * ZigBrains is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with ZigBrains. If not, see . + */ + +package com.falsepattern.zigbrains.lsp.zls.downloader + +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo +import com.falsepattern.zigbrains.shared.downloader.VersionInfo +import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball +import com.falsepattern.zigbrains.shared.downloader.getTarballIfCompatible +import com.falsepattern.zigbrains.shared.downloader.tempPluginDir +import com.intellij.openapi.progress.coroutineToIndicator +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.asSafely +import com.intellij.util.download.DownloadableFileService +import com.intellij.util.text.SemVer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.decodeFromStream +import java.net.URLEncoder + +@JvmRecord +data class ZLSVersionInfo( + override val version: SemVer, + override val date: String, + override val dist: Tarball +): VersionInfo { + companion object { + @OptIn(ExperimentalSerializationApi::class) + suspend fun downloadVersionInfoFor(toolchain: ZigToolchain, project: Project?): List { + return withContext(Dispatchers.IO) { + val zigVersion = toolchain.zig.getEnv(project).getOrNull()?.version ?: return@withContext emptyList() + 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 downloader = service.createDownloader(listOf(desc), "ZLS version information") + val downloadResults = coroutineToIndicator { + downloader.download(tempPluginDir) + } + if (downloadResults.isEmpty()) + return@withContext emptyList() + val index = downloadResults[0].first + val info = index.inputStream().use { Json.decodeFromStream(it) } + index.delete() + return@withContext listOfNotNull(parseVersion(info)) + } + } + } +} +private fun parseVersion(data: JsonObject): ZLSVersionInfo? { + val versionTag = data["version"]?.asSafely()?.content + + val version = SemVer.parseFromText(versionTag) ?: return null + val date = data["date"]?.asSafely()?.content ?: "" + val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) } + ?: return null + + return ZLSVersionInfo(version, date, dist) +} 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 ce6d5ec8..e64d3c7a 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 @@ -24,26 +24,35 @@ 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.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.shared.UUIDMapSerializable import com.falsepattern.zigbrains.shared.ui.ListElem +import com.falsepattern.zigbrains.shared.ui.ListElem.One.Actual 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.asPending +import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.intellij.openapi.ui.NamedConfigurable +import com.intellij.util.text.SemVer +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch import java.awt.Component import java.util.UUID -object ZLSDriver: UUIDComboBoxDriver { +sealed interface ZLSDriver: UUIDComboBoxDriver { override val theMap: UUIDMapSerializable.Converting get() = zlsInstallations - override fun constructModelList(): List> { - return ListElem.fetchGroup() - } - override fun createContext(model: ZBModel): ZBContext { return ZLSContext(null, model) } @@ -52,16 +61,67 @@ object ZLSDriver: UUIDComboBoxDriver { return ZLSComboBox(model) } + override fun createNamedConfigurable(uuid: UUID, elem: ZLSVersion): NamedConfigurable { + return ZLSConfigurable(uuid, elem) + } + override suspend fun resolvePseudo( context: Component, elem: ListElem.Pseudo ): UUID? { - //TODO - return null + return when(elem) { + is ListElem.FromDisk<*> -> ZLSLocalSelector(context).browse() + else -> null + }?.let { zlsInstallations.registerNew(it) } } - override fun createNamedConfigurable(uuid: UUID, elem: ZLSVersion): NamedConfigurable { - //TODO - return ZLSConfigurable(uuid, elem) + object ForList: ZLSDriver { + override fun constructModelList(): List> { + return listOf(ListElem.None(), Separator("", true), ListElem.FromDisk()) + } } + + @JvmRecord + data class ForSelector(private val data: ZigProjectConfigurationProvider.IUserDataBridge?): ZLSDriver { + override fun constructModelList(): List> { + val res = ArrayList>() + res.add(ListElem.None()) + res.add(compatibleInstallations().asPending()) + res.add(Separator("", true)) + res.addAll(ListElem.fetchGroup()) + 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)) + } + } + } + } +} + +private fun numericVersionEquals(a: SemVer, b: SemVer): Boolean { + return a.major == b.major && a.minor == b.minor && a.patch == b.patch } \ 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 f93d1b00..f80d888d 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 @@ -22,27 +22,28 @@ package com.falsepattern.zigbrains.lsp.zls.ui -import com.falsepattern.zigbrains.lsp.zls.ZLSService +import com.falsepattern.zigbrains.lsp.ZLSStarter +import com.falsepattern.zigbrains.lsp.startLSP 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.shared.SubConfigurable -import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain +import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainExtensionsProvider +import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector import com.falsepattern.zigbrains.shared.zigCoroutineScope -import com.intellij.openapi.project.Project -import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.util.Key import com.intellij.ui.dsl.builder.Panel import kotlinx.coroutines.launch -class ZLSEditor(private var project: Project?, - private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): - UUIDMapSelector(ZLSDriver), - SubConfigurable, +class ZLSEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): + UUIDMapSelector(ZLSDriver.ForSelector(sharedState)), + ImmutableElementPanel, ZigProjectConfigurationProvider.UserDataListener { init { - sharedState.addUserDataChangeListener(this) + sharedState?.addUserDataChangeListener(this) } override fun onUserDataChanged(key: Key<*>) { @@ -55,35 +56,30 @@ class ZLSEditor(private var project: Project?, } } - override fun isModified(context: Project): Boolean { - return ZLSService.getInstance(context).zlsUUID != selectedUUID + override fun isModified(toolchain: T): Boolean { + return toolchain.zlsUUID != selectedUUID } - override fun apply(context: Project) { - ZLSService.getInstance(context).zlsUUID = selectedUUID + override fun apply(toolchain: T): T { + return toolchain.withZLS(selectedUUID) } - override fun reset(context: Project?) { - val project = context ?: ProjectManager.getInstance().defaultProject - selectedUUID = ZLSService.getInstance(project).zlsUUID + override fun reset(toolchain: T) { + selectedUUID = toolchain.zlsUUID } override fun dispose() { super.dispose() - sharedState.removeUserDataChangeListener(this) + sharedState?.removeUserDataChangeListener(this) } - override val newProjectBeforeInitSelector: Boolean get() = true - class Provider: ZigProjectConfigurationProvider { - override fun create( - project: Project?, - sharedState: ZigProjectConfigurationProvider.IUserDataBridge - ): SubConfigurable? { - return ZLSEditor(project, sharedState) + class Provider: ZigToolchainExtensionsProvider { + override fun createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): ImmutableElementPanel? { + return ZLSEditor(sharedState) } override val index: Int - get() = 50 + get() = 100 } } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/zls/ui/ZLSListEditor.kt index 3ee4e221..9469478c 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 @@ -28,7 +28,7 @@ import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor import com.intellij.openapi.ui.MasterDetailsComponent import com.intellij.openapi.util.NlsContexts -class ZLSListEditor : UUIDMapEditor(ZLSDriver) { +class ZLSListEditor : UUIDMapEditor(ZLSDriver.ForList) { override fun getEmptySelectionString(): String { return ZLSBundle.message("settings.list.empty") } diff --git a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml index 0182a606..888ca03b 100644 --- a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml +++ b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml @@ -65,7 +65,7 @@ - From 8336d2bcc58ac2c1bd2befea3bac66b1ae8c8b86 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Fri, 11 Apr 2025 16:47:01 +0200 Subject: [PATCH 22/22] 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 + + + + + + + + + + + + +