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.guessProjectDir
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.util.progress.withProgressText
import com.intellij.util.io.awaitExit
@ -140,20 +141,14 @@ class DirenvService(val project: Project): SerializablePersistentStateComponent<
private const val GROUP_DISPLAY_ID = "zigbrains-direnv"
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 {
Auto,
Enabled,
Disabled;
fun isEnabled(project: Project?): Boolean {
return when(this) {
Enabled -> true
Disabled -> false
Auto -> project?.service<DirenvService>()?.hasDotEnv() == true
fun setStateFor(data: UserDataHolder, state: DirenvState) {
data.putUserData(STATE_KEY, state)
}
}
}

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")) {
comboBox(DirenvState.entries).component.let {
cb = it
if (sharedState != null) {
it.addItemListener { e ->
if (e.stateChange != ItemEvent.SELECTED)
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) {
override fun isEnabled(context: Project): DirenvState {
return context.service<DirenvService>().isEnabledRaw
return DirenvService.getInstance(context).isEnabled
}
override fun setEnabled(context: Project, value: DirenvState) {
@ -85,7 +87,7 @@ abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationP
if (project?.isDefault != false) {
return null
}
sharedState.putUserData(DirenvService.STATE_KEY, DirenvState.Auto)
DirenvService.setStateFor(sharedState, DirenvState.Auto)
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.project.toolchain.ZigToolchainListService
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
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<ZigToolchainProvider>("com.falsepattern.zigbrains.toolchainProvider")
@ -53,7 +47,7 @@ internal interface ZigToolchainProvider {
fun serialize(toolchain: ZigToolchain): Map<String, String>
fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean
fun createConfigurable(uuid: UUID, toolchain: ZigToolchain): ZigToolchainConfigurable<*>
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)
}
@ -75,11 +69,11 @@ fun ZigToolchain.createNamedConfigurable(uuid: UUID): ZigToolchainConfigurable<*
}
@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()
return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext ->
val compatibleExisting = existing.filter { ext.isCompatible(it) }
val suggestions = ext.suggestToolchains(project, direnv)
val suggestions = ext.suggestToolchains(project, data)
suggestions.filter { 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()
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 {
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)
}
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.ZigToolchainConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainProvider
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolder
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.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.util.EnvironmentUtil
import com.intellij.util.system.OS
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
import java.util.UUID
@ -97,16 +92,14 @@ class LocalZigToolchainProvider: ZigToolchainProvider {
}
@OptIn(ExperimentalCoroutinesApi::class)
override suspend fun suggestToolchains(project: Project?, direnv: DirenvState): Flow<ZigToolchain> {
val env = if (project != null && direnv.isEnabled(project)) {
override suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow<ZigToolchain> {
val env = if (project != null && DirenvService.getStateFor(data, project).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@flatMapConcat emptyFlow<Path>()
runCatching {
Files.newDirectoryStream(dir).use { stream ->
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.ui.DialogWrapper
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.Panel
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 var editButton: JButton? = null
init {
val direnv = sharedState.getUserData(DirenvService.STATE_KEY) ?: project?.let { DirenvService.getInstance(it).isEnabled } ?: DirenvState.Disabled
model = TCModel(getModelList(project, direnv))
model = TCModel(getModelList(project, sharedState))
toolchainBox = TCComboBox(model)
toolchainBox.addItemListener(::itemStateChanged)
ZigToolchainListService.getInstance().addChangeListener(this)
@ -97,8 +97,7 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
override suspend fun toolchainListChanged() {
withContext(Dispatchers.EDT + toolchainBox.asContextElement()) {
val direnv = sharedState.getUserData(DirenvService.STATE_KEY) ?: project?.let { DirenvService.getInstance(it).isEnabled } ?: DirenvState.Disabled
val list = getModelList(project, direnv)
val list = getModelList(project, sharedState)
model.updateContents(list)
val onReload = selectOnNextReload
selectOnNextReload = null
@ -129,10 +128,8 @@ class ZigToolchainEditor(private var project: Project?, private val sharedState:
}
override fun onUserDataChanged(key: Key<*>) {
if (key == DirenvService.STATE_KEY) {
zigCoroutineScope.launch { toolchainListChanged() }
}
}
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>()
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.add(suggestZigToolchains(project, direnv).asPending())
modelList.add(suggestZigToolchains(project, data).asPending())
return modelList
}