From 3ceb61f2ddd70bda39fa13ff5555546cf8a2d3b7 Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 10 Apr 2025 01:44:43 +0200 Subject: [PATCH] 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 {