async toolchain resolution and other tweaks
This commit is contained in:
parent
c7e33ea8de
commit
3ceb61f2dd
19 changed files with 230 additions and 125 deletions
|
@ -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
|
||||
|
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<DirenvProjectService>()
|
|
@ -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<Map<String, String>>(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()
|
||||
}
|
||||
}
|
|
@ -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<String, String>) {
|
||||
private val path get() = getVariable("PATH")?.split(File.pathSeparatorChar)
|
||||
|
||||
|
@ -55,6 +56,8 @@ data class Env(val env: Map<String, String>) {
|
|||
emit(exePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val emptyEnv = Env(emptyMap())
|
||||
companion object {
|
||||
val empty = Env(emptyMap())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -109,6 +109,18 @@ class ZigToolchainListService: SerializablePersistentStateComponent<ZigToolchain
|
|||
}
|
||||
}
|
||||
|
||||
override fun <T: ZigToolchain> 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 <T: ZigToolchain> withUniqueName(toolchain: T): T
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<ZigToolchainProvider>("com.falsepattern.zigbrains.toolchainProvider")
|
||||
|
@ -41,7 +44,7 @@ internal interface ZigToolchainProvider {
|
|||
fun serialize(toolchain: ZigToolchain): Map<String, String>
|
||||
fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean
|
||||
fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*>
|
||||
fun suggestToolchains(): List<Pair<SemVer?, ZigToolchain>>
|
||||
fun suggestToolchains(): List<Deferred<ZigToolchain>>
|
||||
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<ZigToolchain> {
|
||||
fun suggestZigToolchains(): List<Deferred<ZigToolchain>> {
|
||||
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) {
|
||||
|
|
|
@ -74,7 +74,7 @@ object Downloader {
|
|||
) {
|
||||
version.downloadAndUnpack(downloadPath)
|
||||
}
|
||||
return LocalZigToolchain.tryFromPath(downloadPath)?.second
|
||||
return LocalZigToolchain.tryFromPath(downloadPath)?.let { LocalSelector.browseFromDisk(component, it) }
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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<Boolean>("ZIG_LOCAL_DIRENV")
|
||||
|
||||
|
@ -67,27 +71,23 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override
|
|||
}
|
||||
}
|
||||
|
||||
fun tryFromPathString(pathStr: String?): Pair<SemVer?, LocalZigToolchain>? {
|
||||
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<SemVer?, LocalZigToolchain>? {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Pair<SemVer?, ZigToolchain>> {
|
||||
override fun suggestToolchains(): List<Deferred<ZigToolchain>> {
|
||||
val res = HashSet<String>()
|
||||
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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -184,10 +184,10 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC
|
|||
private fun getModelList(): List<TCListElemIn> {
|
||||
val modelList = ArrayList<TCListElemIn>()
|
||||
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
|
||||
}
|
|
@ -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<TCListElemIn>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>): TCListElem
|
||||
|
||||
companion object {
|
||||
val fetchGroup get() = listOf(Download, FromDisk)
|
||||
|
@ -53,3 +58,5 @@ internal data class Separator(val text: String, val line: Boolean) : TCListElemI
|
|||
internal fun Pair<UUID, ZigToolchain>.asActual() = TCListElem.Toolchain.Actual(first, second)
|
||||
|
||||
internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this)
|
||||
|
||||
internal fun Deferred<ZigToolchain>.asPending() = TCListElem.Pending(zigCoroutineScope.async { this@asPending.await().asSuggested() })
|
|
@ -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<TCListElem>(model) {
|
||||
init {
|
||||
setRenderer(TCCellRenderer({model}))
|
||||
setRenderer(TCCellRenderer { model })
|
||||
}
|
||||
|
||||
var selectedToolchain: UUID?
|
||||
|
@ -86,15 +102,17 @@ internal class TCComboBox(model: TCModel): ComboBox<TCListElem>(model) {
|
|||
}
|
||||
}
|
||||
|
||||
internal class TCModel private constructor(elements: List<TCListElem>, private var separators: Map<TCListElem, Separator>) : CollectionComboBoxModel<TCListElem>(elements) {
|
||||
internal class TCModel private constructor(elements: List<TCListElem>, private var separators: MutableMap<TCListElem, Separator>) : CollectionComboBoxModel<TCListElem>(elements) {
|
||||
private var counter: Int = 0
|
||||
companion object {
|
||||
operator fun invoke(input: List<TCListElemIn>): TCModel {
|
||||
val (elements, separators) = convert(input)
|
||||
val model = TCModel(elements, separators)
|
||||
model.launchPendingResolve()
|
||||
return model
|
||||
}
|
||||
|
||||
private fun convert(input: List<TCListElemIn>): Pair<List<TCListElem>, Map<TCListElem, Separator>> {
|
||||
private fun convert(input: List<TCListElemIn>): Pair<List<TCListElem>, MutableMap<TCListElem, Separator>> {
|
||||
val separators = IdentityHashMap<TCListElem, Separator>()
|
||||
var lastSeparator: Separator? = null
|
||||
val elements = ArrayList<TCListElem>()
|
||||
|
@ -117,10 +135,52 @@ internal class TCModel private constructor(elements: List<TCListElem>, 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..<size) {
|
||||
val elem = getElementAt(i)
|
||||
?: continue
|
||||
if (elem !is TCListElem.Pending)
|
||||
continue
|
||||
zigCoroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
val newElem = elem.elem.awaitResult().getOrNull()
|
||||
swap(elem, newElem, counter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
private fun swap(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)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun updateContents(input: List<TCListElemIn>) {
|
||||
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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue