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