tristate direnv config
This commit is contained in:
parent
3ceb61f2dd
commit
f7ea73ae45
17 changed files with 326 additions and 118 deletions
|
@ -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
|
||||
|
|
|
@ -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")
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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>> (
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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() })
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue