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
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

View file

@ -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<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)
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<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
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<T: ZigExecConfig<T>>(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
}

View file

@ -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<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.ui.RunContentDescriptor
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.blockingContext
class ZigRegularRunner: ZigProgramRunner<ZigProfileState<*>>(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)

View file

@ -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<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.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<Project>?
fun create(project: Project?, sharedState: IUserDataBridge): SubConfigurable<Project>?
val index: Int
companion object {
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigProjectConfigurationProvider>("com.falsepattern.zigbrains.projectConfigProvider")
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
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<ZigToolchainProvider>("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<String, String>): ZigToolchain?
fun serialize(toolchain: ZigToolchain): Map<String, String>
fun matchesSuggestion(toolchain: ZigToolchain, suggestion: ZigToolchain): Boolean
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)
}
@ -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<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()
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) {

View file

@ -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<Boolean>("ZIG_LOCAL_DIRENV")
@Throws(ExecutionException::class)
fun ensureLocal(toolchain: ZigToolchain): LocalZigToolchain {
if (toolchain is LocalZigToolchain) {

View file

@ -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<Deferred<ZigToolchain>> {
val res = HashSet<String>()
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<ZigToolchain> {
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<Path>()
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) {

View file

@ -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<Project>, ToolchainListChangeListener {
class ZigToolchainEditor(private var project: Project?, private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>, 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<Project>? {
return ZigToolchainEditor(project?.isDefault ?: false).also { it.reset(project) }
override fun create(project: Project?, sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
return ZigToolchainEditor(project, sharedState).also { it.reset(project) }
}
override val index: Int get() = 0
}
}
private fun getModelList(): List<TCListElemIn> {
private fun getModelList(project: Project?, direnv: DirenvState): 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.addAll(suggestZigToolchains().map { it.asPending() })
modelList.add(suggestZigToolchains(project, direnv).asPending())
return modelList
}

View file

@ -66,7 +66,7 @@ class ZigToolchainListEditor : MasterDetailsComponent(), ToolchainListChangeList
val modelList = ArrayList<TCListElemIn>()
modelList.addAll(TCListElem.fetchGroup)
modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true))
modelList.addAll(suggestZigToolchains().map { it.asPending() })
modelList.add(suggestZigToolchains().asPending())
val model = TCModel(modelList)
val context = TCContext(null, model)
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.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>): TCListElem
data class Pending(val elems: Flow<TCListElem>): TCListElem
companion object {
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 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
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<TCListElem>, 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

View file

@ -190,6 +190,9 @@
<projectConfigProvider
implementation="com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainEditor$Provider"
/>
<projectConfigProvider
implementation="com.falsepattern.zigbrains.direnv.ui.DirenvEditor$Provider"
/>
</extensions>
<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-named=Toolchain already exists as "{0}"
settings.toolchain.local-selector.state.ok=Toolchain path OK
settings.direnv.enable.label=Direnv