direnv decoupling

This commit is contained in:
FalsePattern 2025-04-10 11:27:22 +02:00
parent f7ea73ae45
commit 68b60e2c77
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
7 changed files with 79 additions and 47 deletions

View file

@ -33,6 +33,7 @@ import com.intellij.openapi.components.*
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.util.Key
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.vfs.toNioPathOrNull 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
@ -140,20 +141,14 @@ class DirenvService(val project: Project): SerializablePersistentStateComponent<
private const val GROUP_DISPLAY_ID = "zigbrains-direnv" private const val GROUP_DISPLAY_ID = "zigbrains-direnv"
fun getInstance(project: Project): IDirenvService = project.service<DirenvService>() fun getInstance(project: Project): IDirenvService = project.service<DirenvService>()
val STATE_KEY = Key.create<DirenvState>("DIRENV_STATE") private val STATE_KEY = Key.create<DirenvState>("DIRENV_STATE")
fun getStateFor(data: UserDataHolder, project: Project?): DirenvState {
return data.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled
} }
}
enum class DirenvState { fun setStateFor(data: UserDataHolder, state: DirenvState) {
Auto, data.putUserData(STATE_KEY, state)
Enabled,
Disabled;
fun isEnabled(project: Project?): Boolean {
return when(this) {
Enabled -> true
Disabled -> false
Auto -> project?.service<DirenvService>()?.hasDotEnv() == true
} }
} }
} }

View file

@ -0,0 +1,40 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.direnv
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
enum class DirenvState {
Auto,
Enabled,
Disabled;
fun isEnabled(project: Project?): Boolean {
return when(this) {
Enabled -> true
Disabled -> false
Auto -> project?.service<DirenvService>()?.hasDotEnv() == true
}
}
}

View file

@ -39,10 +39,12 @@ abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationP
row(ZigBrainsBundle.message("settings.direnv.enable.label")) { row(ZigBrainsBundle.message("settings.direnv.enable.label")) {
comboBox(DirenvState.entries).component.let { comboBox(DirenvState.entries).component.let {
cb = it cb = it
if (sharedState != null) {
it.addItemListener { e -> it.addItemListener { e ->
if (e.stateChange != ItemEvent.SELECTED) if (e.stateChange != ItemEvent.SELECTED)
return@addItemListener return@addItemListener
sharedState DirenvService.setStateFor(sharedState, DirenvState.Auto)
}
} }
} }
} }
@ -72,7 +74,7 @@ abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationP
class ForProject(sharedState: ZigProjectConfigurationProvider.IUserDataBridge) : DirenvEditor<Project>(sharedState) { class ForProject(sharedState: ZigProjectConfigurationProvider.IUserDataBridge) : DirenvEditor<Project>(sharedState) {
override fun isEnabled(context: Project): DirenvState { override fun isEnabled(context: Project): DirenvState {
return context.service<DirenvService>().isEnabledRaw return DirenvService.getInstance(context).isEnabled
} }
override fun setEnabled(context: Project, value: DirenvState) { override fun setEnabled(context: Project, value: DirenvState) {
@ -85,7 +87,7 @@ abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationP
if (project?.isDefault != false) { if (project?.isDefault != false) {
return null return null
} }
sharedState.putUserData(DirenvService.STATE_KEY, DirenvState.Auto) DirenvService.setStateFor(sharedState, DirenvState.Auto)
return ForProject(sharedState) return ForProject(sharedState)
} }

View file

@ -24,24 +24,18 @@ package com.falsepattern.zigbrains.project.toolchain.base
import com.falsepattern.zigbrains.direnv.DirenvState 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.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.UserDataHolder
import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleColoredComponent
import com.intellij.util.text.SemVer
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMap
import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flowOn 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")
@ -53,7 +47,7 @@ internal interface ZigToolchainProvider {
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<*>
suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow<ZigToolchain> suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow<ZigToolchain>
fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean)
} }
@ -75,11 +69,11 @@ fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun suggestZigToolchains(project: Project? = null, direnv: DirenvState = DirenvState.Disabled): Flow<ZigToolchain> { fun suggestZigToolchains(project: Project? = null, data: UserDataHolder = emptyData): 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.asFlow().flatMapConcat { 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(project, direnv) val suggestions = ext.suggestToolchains(project, data)
suggestions.filter { suggestion -> suggestions.filter { suggestion ->
compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) } compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) }
} }
@ -90,3 +84,14 @@ fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean
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.render(this, component, isSuggestion, isSelected) return provider.render(this, component, isSuggestion, isSelected)
} }
private val emptyData = object: UserDataHolder {
override fun <T : Any?> getUserData(key: Key<T?>): T? {
return null
}
override fun <T : Any?> putUserData(key: Key<T?>, value: T?) {
}
}

View file

@ -40,7 +40,7 @@ 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 {
if (project != null && (commandLine.getUserData(DirenvService.STATE_KEY) ?: DirenvService.getInstance(project).isEnabled).isEnabled(project)) { if (project != null && DirenvService.getStateFor(commandLine, project).isEnabled(project)) {
commandLine.withEnvironment(DirenvService.getInstance(project).import().env) commandLine.withEnvironment(DirenvService.getInstance(project).import().env)
} }
return commandLine return commandLine

View file

@ -28,7 +28,6 @@ import com.falsepattern.zigbrains.direnv.Env
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolder import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
@ -36,19 +35,15 @@ import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.SimpleColoredComponent import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes import com.intellij.ui.SimpleTextAttributes
import com.intellij.util.EnvironmentUtil
import com.intellij.util.system.OS import com.intellij.util.system.OS
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapConcat import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flattenConcat import kotlinx.coroutines.flow.flattenConcat
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.UUID import java.util.UUID
@ -97,16 +92,14 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
override suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow<ZigToolchain> { override suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow<ZigToolchain> {
val env = if (project != null && direnv.isEnabled(project)) { val env = if (project != null && DirenvService.getStateFor(data, project).isEnabled(project)) {
DirenvService.getInstance(project).import() DirenvService.getInstance(project).import()
} else { } else {
Env.empty Env.empty
} }
val pathToolchains = env.findAllExecutablesOnPATH("zig").mapNotNull { it.parent } val pathToolchains = env.findAllExecutablesOnPATH("zig").mapNotNull { it.parent }
val wellKnown = getWellKnown().asFlow().flatMapConcat { dir -> val wellKnown = getWellKnown().asFlow().flatMapConcat { dir ->
if (!dir.isDirectory())
return@flatMapConcat emptyFlow<Path>()
runCatching { runCatching {
Files.newDirectoryStream(dir).use { stream -> Files.newDirectoryStream(dir).use { stream ->
stream.toList().filterNotNull().asFlow() stream.toList().filterNotNull().asFlow()

View file

@ -43,6 +43,7 @@ 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.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder
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
@ -60,8 +61,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
private val model: TCModel private val model: TCModel
private var editButton: JButton? = null private var editButton: JButton? = null
init { init {
val direnv = sharedState.getUserData(DirenvService.STATE_KEY) ?: project?.let { DirenvService.getInstance(it).isEnabled } ?: DirenvState.Disabled model = TCModel(getModelList(project, sharedState))
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)
@ -97,8 +97,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
override suspend fun toolchainListChanged() { override suspend fun toolchainListChanged() {
withContext(Dispatchers.EDT + toolchainBox.asContextElement()) { withContext(Dispatchers.EDT + toolchainBox.asContextElement()) {
val direnv = sharedState.getUserData(DirenvService.STATE_KEY) ?: project?.let { DirenvService.getInstance(it).isEnabled } ?: DirenvState.Disabled val list = getModelList(project, sharedState)
val list = getModelList(project, direnv)
model.updateContents(list) model.updateContents(list)
val onReload = selectOnNextReload val onReload = selectOnNextReload
selectOnNextReload = null selectOnNextReload = null
@ -129,10 +128,8 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
} }
override fun onUserDataChanged(key: Key<*>) { override fun onUserDataChanged(key: Key<*>) {
if (key == DirenvService.STATE_KEY) {
zigCoroutineScope.launch { toolchainListChanged() } zigCoroutineScope.launch { toolchainListChanged() }
} }
}
override fun attach(p: Panel): Unit = with(p) { override fun attach(p: Panel): Unit = with(p) {
@ -199,13 +196,13 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
} }
private fun getModelList(project: Project?, direnv: DirenvState): List<TCListElemIn> { private fun getModelList(project: Project?, data: UserDataHolder): 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.add(suggestZigToolchains(project, direnv).asPending()) modelList.add(suggestZigToolchains(project, data).asPending())
return modelList return modelList
} }