tristate direnv config

This commit is contained in:
FalsePattern 2025-04-10 04:14:31 +02:00
parent 3ceb61f2dd
commit f7ea73ae45
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
17 changed files with 326 additions and 118 deletions

View file

@ -22,8 +22,6 @@
package com.falsepattern.zigbrains 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.BrowserUtil
import com.intellij.ide.plugins.PluginManager import com.intellij.ide.plugins.PluginManager
import com.intellij.notification.Notification 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.options.ShowSettingsUtil
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.UserDataHolderBase
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import java.lang.reflect.Method import java.lang.reflect.Method
import kotlin.io.path.pathString
class ZBStartup: ProjectActivity { class ZBStartup: ProjectActivity {
var firstInit = true var firstInit = true

View file

@ -29,24 +29,47 @@ import com.intellij.ide.impl.isTrusted
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications import com.intellij.notification.Notifications
import com.intellij.openapi.components.Service import com.intellij.openapi.components.*
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir 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.platform.util.progress.withProgressText
import com.intellij.util.io.awaitExit import com.intellij.util.io.awaitExit
import com.intellij.util.xmlb.annotations.Attribute
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.isRegularFile
@Service(Service.Level.PROJECT) @Service(Service.Level.PROJECT)
class DirenvService(val project: Project) { @State(
val mutex = Mutex() name = "Direnv",
storages = [Storage("zigbrains.xml")]
)
class DirenvService(val project: Project): SerializablePersistentStateComponent<DirenvService.State>(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) if (!isInstalled || !project.isTrusted() || project.isDefault)
return Env.empty return Env.empty
val workDir = project.guessProjectDir()?.toNioPath() ?: return Env.empty val workDir = project.guessProjectDir()?.toNioPath() ?: return Env.empty
@ -100,13 +123,45 @@ class DirenvService(val project: Project) {
return DirenvOutput(stdOut, false) return DirenvOutput(stdOut, false)
} }
val isInstalled: Boolean by lazy { fun hasDotEnv(): Boolean {
// Using the builtin stuff here instead of Env because it should only scan for direnv on the process path if (!isInstalled)
PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("direnv") != null 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 { companion object {
private const val GROUP_DISPLAY_ID = "zigbrains-direnv" private const val GROUP_DISPLAY_ID = "zigbrains-direnv"
fun getInstance(project: Project): DirenvService = project.service() fun getInstance(project: Project): IDirenvService = project.service<DirenvService>()
val STATE_KEY = Key.create<DirenvState>("DIRENV_STATE")
} }
} }
enum class DirenvState {
Auto,
Enabled,
Disabled;
fun isEnabled(project: Project?): Boolean {
return when(this) {
Enabled -> true
Disabled -> false
Auto -> project?.service<DirenvService>()?.hasDotEnv() == true
}
}
}
sealed interface IDirenvService {
val isInstalled: Boolean
val isEnabled: DirenvState
suspend fun import(): Env
}
private val envFiles = listOf(".envrc", ".env")

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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<T>(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): SubConfigurable<T> {
private var cb: ComboBox<DirenvState>? = 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<Project>(sharedState) {
override fun isEnabled(context: Project): DirenvState {
return context.service<DirenvService>().isEnabledRaw
}
override fun setEnabled(context: Project, value: DirenvState) {
context.service<DirenvService>().isEnabledRaw = value
}
}
class Provider: ZigProjectConfigurationProvider {
override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
if (project?.isDefault != false) {
return null
}
sharedState.putUserData(DirenvService.STATE_KEY, DirenvState.Auto)
return ForProject(sharedState)
}
override val index: Int
get() = 1
}
}

View file

@ -22,6 +22,7 @@
package com.falsepattern.zigbrains.project.execution.base package com.falsepattern.zigbrains.project.execution.base
import com.falsepattern.zigbrains.direnv.DirenvService
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory 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.project.guessProjectDir
import com.intellij.openapi.util.NlsActions.ActionText import com.intellij.openapi.util.NlsActions.ActionText
import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.util.progress.reportRawProgress
import org.jdom.Element import org.jdom.Element
import org.jetbrains.annotations.Nls import org.jetbrains.annotations.Nls
@ -62,10 +64,10 @@ abstract class ZigExecConfig<T: ZigExecConfig<T>>(project: Project, factory: Con
suspend fun patchCommandLine(commandLine: GeneralCommandLine): GeneralCommandLine { suspend fun patchCommandLine(commandLine: GeneralCommandLine): GeneralCommandLine {
// TODO direnv val direnv = DirenvService.getInstance(project)
// if (project.zigProjectSettings.state.direnv) { if (direnv.isEnabled.isEnabled(project)) {
// commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) commandLine.withEnvironment(direnv.import().env)
// } }
return commandLine return commandLine
} }

View file

@ -35,6 +35,8 @@ import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.platform.ide.progress.ModalTaskOwner 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 import kotlin.io.path.pathString
abstract class ZigProfileState<T: ZigExecConfig<T>> ( abstract class ZigProfileState<T: ZigExecConfig<T>> (

View file

@ -32,10 +32,13 @@ import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.RunContentBuilder import com.intellij.execution.runners.RunContentBuilder
import com.intellij.execution.ui.RunContentDescriptor import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.blockingContext
class ZigRegularRunner: ZigProgramRunner<ZigProfileState<*>>(DefaultRunExecutor.EXECUTOR_ID) { class ZigRegularRunner: ZigProgramRunner<ZigProfileState<*>>(DefaultRunExecutor.EXECUTOR_ID) {
override suspend fun execute(state: ZigProfileState<*>, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? { 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()) { return withEDTContext(ModalityState.any()) {
val runContentBuilder = RunContentBuilder(exec, environment) val runContentBuilder = RunContentBuilder(exec, environment)
runContentBuilder.showRunContent(null) runContentBuilder.showRunContent(null)

View file

@ -26,6 +26,7 @@ import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsContexts import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.UserDataHolderBase
class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter<Project>() { class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter<Project>() {
override fun instantiate(): List<SubConfigurable<Project>> { override fun instantiate(): List<SubConfigurable<Project>> {

View file

@ -25,14 +25,51 @@ package com.falsepattern.zigbrains.project.settings
import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project 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 { interface ZigProjectConfigurationProvider {
fun create(project: Project?): SubConfigurable<Project>? fun create(project: Project?, sharedState: IUserDataBridge): SubConfigurable<Project>?
val index: Int val index: Int
companion object { companion object {
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigProjectConfigurationProvider>("com.falsepattern.zigbrains.projectConfigProvider") private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigProjectConfigurationProvider>("com.falsepattern.zigbrains.projectConfigProvider")
fun createPanels(project: Project?): List<SubConfigurable<Project>> { fun createPanels(project: Project?): List<SubConfigurable<Project>> {
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) }
}
}
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<UserDataListener>()
override fun <T : Any?> putUserData(key: Key<T?>, 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)
}
} }
} }
} }

View file

@ -22,6 +22,7 @@
package com.falsepattern.zigbrains.project.toolchain.base package com.falsepattern.zigbrains.project.toolchain.base
import com.falsepattern.zigbrains.direnv.DirenvState
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.extensions.ExtensionPointName import com.intellij.openapi.extensions.ExtensionPointName
@ -30,21 +31,29 @@ import com.intellij.openapi.util.UserDataHolder
import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleColoredComponent
import com.intellij.util.text.SemVer import com.intellij.util.text.SemVer
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async 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 import java.util.UUID
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainProvider>("com.falsepattern.zigbrains.toolchainProvider") private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainProvider>("com.falsepattern.zigbrains.toolchainProvider")
internal interface ZigToolchainProvider { internal interface ZigToolchainProvider {
suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): ZigToolchain?
val serialMarker: String val serialMarker: String
fun isCompatible(toolchain: ZigToolchain): Boolean fun isCompatible(toolchain: ZigToolchain): Boolean
fun deserialize(data: Map<String, String>): ZigToolchain? fun deserialize(data: Map<String, String>): ZigToolchain?
fun serialize(toolchain: ZigToolchain): Map<String, String> fun serialize(toolchain: ZigToolchain): Map<String, String>
fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean
fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*> fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*>
fun suggestToolchains(): List<Deferred<ZigToolchain>> suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow<ZigToolchain>
fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) 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)) 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<*> { fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*> {
val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException() val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException()
return provider.createConfigurable(uuid, this) return provider.createConfigurable(uuid, this)
} }
fun suggestZigToolchains(): List<Deferred<ZigToolchain>> { @OptIn(ExperimentalCoroutinesApi::class)
fun suggestZigToolchains(project: Project? = null, direnv: DirenvState = DirenvState.Disabled): Flow<ZigToolchain> {
val existing = ZigToolchainListService.getInstance().toolchains.map { (_, tc) -> tc }.toList() 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 compatibleExisting = existing.filter { ext.isCompatible(it) }
val suggestions = ext.suggestToolchains() val suggestions = ext.suggestToolchains(project, direnv)
suggestions.map { suggestion -> suggestions.filter { suggestion ->
zigCoroutineScope.async { compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) }
val sugg = suggestion.await()
if (compatibleExisting.none { existing -> ext.matchesSuggestion(existing, sugg) }) {
sugg
} else {
throw IllegalArgumentException()
}
}
}
} }
}.flowOn(Dispatchers.IO)
} }
fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) {

View file

@ -22,18 +22,16 @@
package com.falsepattern.zigbrains.project.toolchain.local package com.falsepattern.zigbrains.project.toolchain.local
import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.KeyWithDefaultValue
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull
import kotlinx.coroutines.delay
import java.nio.file.Path import java.nio.file.Path
import kotlin.random.Random
@JvmRecord @JvmRecord
data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): ZigToolchain { data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null): 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 { override suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project?): GeneralCommandLine {
//TODO direnv if (project != null && (commandLine.getUserData(DirenvService.STATE_KEY) ?: DirenvService.getInstance(project).isEnabled).isEnabled(project)) {
// if (project != null && (commandLine.getUserData(DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { commandLine.withEnvironment(DirenvService.getInstance(project).import().env)
// commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) }
// }
return commandLine return commandLine
} }
@ -59,8 +56,6 @@ data class LocalZigToolchain(val location: Path, val std: Path? = null, override
} }
companion object { companion object {
val DIRENV_KEY = KeyWithDefaultValue.create<Boolean>("ZIG_LOCAL_DIRENV")
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
fun ensureLocal(toolchain: ZigToolchain): LocalZigToolchain { fun ensureLocal(toolchain: ZigToolchain): LocalZigToolchain {
if (toolchain is LocalZigToolchain) { if (toolchain is LocalZigToolchain) {

View file

@ -22,6 +22,8 @@
package com.falsepattern.zigbrains.project.toolchain.local 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.direnv.Env
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable 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.ui.SimpleTextAttributes
import com.intellij.util.EnvironmentUtil import com.intellij.util.EnvironmentUtil
import com.intellij.util.system.OS import com.intellij.util.system.OS
import kotlinx.coroutines.Deferred import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async 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.io.File
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@ -46,18 +56,6 @@ import kotlin.io.path.isDirectory
import kotlin.io.path.pathString import kotlin.io.path.pathString
class LocalZigToolchainProvider: ZigToolchainProvider { 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 override val serialMarker: String
get() = "local" get() = "local"
@ -98,22 +96,25 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
return LocalZigToolchainConfigurable(uuid, toolchain) return LocalZigToolchainConfigurable(uuid, toolchain)
} }
override fun suggestToolchains(): List<Deferred<ZigToolchain>> { @OptIn(ExperimentalCoroutinesApi::class)
val res = HashSet<String>() override suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow<ZigToolchain> {
EnvironmentUtil.getValue("PATH")?.split(File.pathSeparatorChar)?.let { res.addAll(it.toList()) } val env = if (project != null && direnv.isEnabled(project)) {
val wellKnown = getWellKnown() DirenvService.getInstance(project).import()
wellKnown.forEach { dir -> } else {
Env.empty
}
val pathToolchains = env.findAllExecutablesOnPATH("zig").mapNotNull { it.parent }
val wellKnown = getWellKnown().asFlow().flatMapConcat { dir ->
if (!dir.isDirectory()) if (!dir.isDirectory())
return@forEach return@flatMapConcat emptyFlow<Path>()
runCatching { runCatching {
Files.newDirectoryStream(dir).use { stream -> Files.newDirectoryStream(dir).use { stream ->
stream.forEach { subDir -> stream.toList().filterNotNull().asFlow()
res.add(subDir.pathString)
} }
}.getOrElse { emptyFlow() }
} }
} val joined = flowOf(pathToolchains, wellKnown).flattenConcat()
} return joined.mapNotNull { LocalZigToolchain.tryFromPath(it) }
return res.map { zigCoroutineScope.async { LocalZigToolchain.tryFromPathString(it) ?: throw IllegalArgumentException() } }
} }
override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) { override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) {
@ -121,21 +122,20 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
val name = toolchain.name val name = toolchain.name
val path = presentDetectedPath(toolchain.location.pathString) val path = presentDetectedPath(toolchain.location.pathString)
val primary: String val primary: String
val secondary: String? var secondary: String?
val tooltip: String? val tooltip: String?
if (isSuggestion) { if (isSuggestion) {
primary = path primary = path
secondary = name secondary = name
tooltip = null
} else { } else {
primary = name ?: "Zig" primary = name ?: "Zig"
if (isSelected) {
secondary = null
tooltip = path
} else {
secondary = path secondary = path
tooltip = null
} }
if (isSelected) {
tooltip = secondary
secondary = null
} else {
tooltip = null
} }
component.append(primary) component.append(primary)
if (secondary != null) { if (secondary != null) {

View file

@ -23,6 +23,8 @@
package com.falsepattern.zigbrains.project.toolchain.ui package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.direnv.DirenvState
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener import com.falsepattern.zigbrains.project.toolchain.ToolchainListChangeListener
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService 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.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.application.EDT import com.intellij.openapi.application.EDT
import com.intellij.openapi.observable.util.whenListChanged
import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.ui.DialogWrapper 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.AlignX
import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Panel
import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.concurrency.annotations.RequiresEdt
@ -50,16 +54,24 @@ import java.util.UUID
import javax.swing.JButton import javax.swing.JButton
import kotlin.collections.addAll import kotlin.collections.addAll
class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubConfigurable<Project>, ToolchainListChangeListener { class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>, ToolchainListChangeListener, ZigProjectConfigurationProvider.UserDataListener {
private val toolchainBox: TCComboBox private val toolchainBox: TCComboBox
private var selectOnNextReload: UUID? = null private var selectOnNextReload: UUID? = null
private val model: TCModel private val model: TCModel
private var editButton: JButton? = null private var editButton: JButton? = null
init { 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 = TCComboBox(model)
toolchainBox.addItemListener(::itemStateChanged) toolchainBox.addItemListener(::itemStateChanged)
ZigToolchainListService.getInstance().addChangeListener(this) ZigToolchainListService.getInstance().addChangeListener(this)
sharedState.addUserDataChangeListener(this)
model.whenListChanged {
if (toolchainBox.isPopupVisible) {
toolchainBox.isPopupVisible = false
toolchainBox.isPopupVisible = true
}
}
} }
private fun refreshButtonState(item: Any?) { private fun refreshButtonState(item: Any?) {
@ -85,7 +97,8 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC
override suspend fun toolchainListChanged() { override suspend fun toolchainListChanged() {
withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { 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) model.updateContents(list)
val onReload = selectOnNextReload val onReload = selectOnNextReload
selectOnNextReload = null 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) { override fun attach(p: Panel): Unit = with(p) {
row(ZigBrainsBundle.message( row(ZigBrainsBundle.message(
if (isForDefaultProject) if (project?.isDefault == true)
"settings.toolchain.editor.toolchain-default.label" "settings.toolchain.editor.toolchain-default.label"
else else
"settings.toolchain.editor.toolchain.label") "settings.toolchain.editor.toolchain.label")
@ -169,25 +189,23 @@ class ZigToolchainEditor(private val isForDefaultProject: Boolean = false): SubC
} }
override val newProjectBeforeInitSelector get() = true override val newProjectBeforeInitSelector get() = true
class Provider: ZigProjectConfigurationProvider { class Provider: ZigProjectConfigurationProvider {
override fun create(project: Project?): SubConfigurable<Project>? { override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
return ZigToolchainEditor(project?.isDefault ?: false).also { it.reset(project) } return ZigToolchainEditor(project, sharedState).also { it.reset(project) }
} }
override val index: Int get() = 0 override val index: Int get() = 0
} }
} }
private fun getModelList(): List<TCListElemIn> { private fun getModelList(project: Project?, direnv: DirenvState): List<TCListElemIn> {
val modelList = ArrayList<TCListElemIn>() val modelList = ArrayList<TCListElemIn>()
modelList.add(TCListElem.None) modelList.add(TCListElem.None)
modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }.sortedBy { it.toolchain.name }) modelList.addAll(ZigToolchainListService.getInstance().toolchains.map { it.asActual() }.sortedBy { it.toolchain.name })
modelList.add(Separator("", true)) modelList.add(Separator("", true))
modelList.addAll(TCListElem.fetchGroup) modelList.addAll(TCListElem.fetchGroup)
modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) 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 return modelList
} }

View file

@ -66,7 +66,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList
val modelList = ArrayList<TCListElemIn>() val modelList = ArrayList<TCListElemIn>()
modelList.addAll(TCListElem.fetchGroup) modelList.addAll(TCListElem.fetchGroup)
modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true)) 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 model = TCModel(modelList)
val context = TCContext(null, model) val context = TCContext(null, model)
val popup = TCComboBoxPopup(context, null, ::onItemSelected) val popup = TCComboBoxPopup(context, null, ::onItemSelected)

View file

@ -24,9 +24,9 @@ package com.falsepattern.zigbrains.project.toolchain.ui
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.UUID import java.util.UUID
@ -45,7 +45,7 @@ internal sealed interface TCListElem : TCListElemIn {
object None: TCListElem object None: TCListElem
object Download : TCListElem, Pseudo object Download : TCListElem, Pseudo
object FromDisk : TCListElem, Pseudo object FromDisk : TCListElem, Pseudo
data class Pending(val elem: Deferred<TCListElem>): TCListElem data class Pending(val elems: Flow<TCListElem>): TCListElem
companion object { companion object {
val fetchGroup get() = listOf(Download, FromDisk) val fetchGroup get() = listOf(Download, FromDisk)
@ -59,4 +59,4 @@ internal fun Pair<UUID, ZigToolchain>.asActual() = TCListElem.Toolchain.Actual(f
internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this) internal fun ZigToolchain.asSuggested() = TCListElem.Toolchain.Suggested(this)
internal fun Deferred<ZigToolchain>.asPending() = TCListElem.Pending(zigCoroutineScope.async { this@asPending.await().asSuggested() }) internal fun Flow<ZigToolchain>.asPending() = TCListElem.Pending(map { it.asSuggested() })

View file

@ -22,7 +22,6 @@
package com.falsepattern.zigbrains.project.toolchain.ui package com.falsepattern.zigbrains.project.toolchain.ui
import ai.grazie.utils.attributes.value
import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.base.render 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.EDT
import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.application.impl.ModalityStateEx
import com.intellij.openapi.application.runInEdt import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.CellRendererPanel import com.intellij.ui.CellRendererPanel
import com.intellij.ui.ClientProperty
import com.intellij.ui.CollectionComboBoxModel import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.ComponentUtil
import com.intellij.ui.GroupHeaderSeparator import com.intellij.ui.GroupHeaderSeparator
import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes 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.EmptyIcon
import com.intellij.util.ui.JBUI import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil import com.intellij.util.ui.UIUtil
import fleet.util.async.awaitResult
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.awt.BorderLayout import java.awt.BorderLayout
import java.awt.Component import java.awt.Component
import java.util.IdentityHashMap import java.util.IdentityHashMap
import java.util.UUID import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
import javax.accessibility.AccessibleContext import javax.accessibility.AccessibleContext
import javax.swing.CellRendererPane
import javax.swing.JComponent
import javax.swing.JList import javax.swing.JList
import javax.swing.border.Border import javax.swing.border.Border
@ -145,33 +136,43 @@ internal class TCModel private constructor(elements: List<TCListElem>, private v
if (elem !is TCListElem.Pending) if (elem !is TCListElem.Pending)
continue continue
zigCoroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) { zigCoroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) {
val newElem = elem.elem.awaitResult().getOrNull() elem.elems.collect { newElem ->
swap(elem, newElem, counter) insertBefore(elem, newElem, counter)
}
remove(elem, counter)
} }
} }
} }
} }
@RequiresEdt @RequiresEdt
private fun swap(old: TCListElem, new: TCListElem?, oldCounter: Int) { private fun remove(old: TCListElem, oldCounter: Int) {
val newCounter = this@TCModel.counter val newCounter = this@TCModel.counter
if (oldCounter != newCounter) { if (oldCounter != newCounter) {
return return
} }
if (new == null) {
val index = this@TCModel.getElementIndex(old) val index = this@TCModel.getElementIndex(old)
this@TCModel.remove(index) this@TCModel.remove(index)
val sep = separators.remove(old) val sep = separators.remove(old)
if (sep != null && this@TCModel.size > index) { if (sep != null && this@TCModel.size > index) {
this@TCModel.getElementAt(index)?.let { separators[it] = sep } this@TCModel.getElementAt(index)?.let { separators[it] = sep }
} }
}
@RequiresEdt
private fun insertBefore(old: TCListElem, new: TCListElem?, oldCounter: Int) {
val newCounter = this@TCModel.counter
if (oldCounter != newCounter) {
return
}
if (new == null) {
return return
} }
val currentIndex = this@TCModel.getElementIndex(old) val currentIndex = this@TCModel.getElementIndex(old)
separators.remove(old)?.let { separators.remove(old)?.let {
separators.put(new, it) separators.put(new, it)
} }
this@TCModel.setElementAt(new, currentIndex) this@TCModel.add(currentIndex, new)
} }
@RequiresEdt @RequiresEdt

View file

@ -190,6 +190,9 @@
<projectConfigProvider <projectConfigProvider
implementation="com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainEditor$Provider" implementation="com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainEditor$Provider"
/> />
<projectConfigProvider
implementation="com.falsepattern.zigbrains.direnv.ui.DirenvEditor$Provider"
/>
</extensions> </extensions>
<actions resource-bundle="zigbrains.ActionsBundle"> <actions resource-bundle="zigbrains.ActionsBundle">

View file

@ -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-unnamed=Toolchain already exists
settings.toolchain.local-selector.state.already-exists-named=Toolchain already exists as "{0}" settings.toolchain.local-selector.state.already-exists-named=Toolchain already exists as "{0}"
settings.toolchain.local-selector.state.ok=Toolchain path OK settings.toolchain.local-selector.state.ok=Toolchain path OK
settings.direnv.enable.label=Direnv