feat!: Toolchain rework

This commit is contained in:
FalsePattern 2025-04-11 17:19:21 +02:00
commit d5359e5816
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
111 changed files with 4980 additions and 1362 deletions

View file

@ -25,6 +25,11 @@ which are the property of the Zig Software Foundation.
(https://github.com/ziglang/logo) (https://github.com/ziglang/logo)
These art assets are licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0). These art assets are licensed under Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0).
-------------------------------- --------------------------------
The art assets inside src/art/zls, and all copies of them, are derived from the Zig Language Server,
which are the property of the zigtools organization.
(https://github.com/zigtools/zls)
These art assets are licensed under MIT license.
--------------------------------
Parts of the codebase are based on the intellij-zig plugin, Parts of the codebase are based on the intellij-zig plugin,
developed by HTGAzureX1212 (https://github.com/HTGAzureX1212), licensed under the Apache 2.0 license. developed by HTGAzureX1212 (https://github.com/HTGAzureX1212), licensed under the Apache 2.0 license.
-------------------------------- --------------------------------
@ -38,3 +43,4 @@ All of the licenses listed here are available in the following files, bundled wi
- licenses/GPL3.LICENSE - licenses/GPL3.LICENSE
- licenses/INTELLIJ-RUST.LICENSE - licenses/INTELLIJ-RUST.LICENSE
- licenses/LGPL3.LICENSE - licenses/LGPL3.LICENSE
- licenses/ZLS.LICENSE

View file

@ -24,14 +24,14 @@ package com.falsepattern.zigbrains.debugger.execution.binary
import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.debugger.ZigDebugBundle
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.ExecutionEnvironment
import kotlin.io.path.pathString import kotlin.io.path.pathString
class ZigProfileStateBinary(environment: ExecutionEnvironment, configuration: ZigExecConfigBinary) : ZigProfileState<ZigExecConfigBinary>(environment, configuration) { class ZigProfileStateBinary(environment: ExecutionEnvironment, configuration: ZigExecConfigBinary) : ZigProfileState<ZigExecConfigBinary>(environment, configuration) {
override suspend fun getCommandLine(toolchain: AbstractZigToolchain, debug: Boolean): GeneralCommandLine { override suspend fun getCommandLine(toolchain: ZigToolchain, debug: Boolean): GeneralCommandLine {
val cli = GeneralCommandLine() val cli = GeneralCommandLine()
val cfg = configuration val cfg = configuration
cfg.workingDirectory.path?.let { cli.withWorkingDirectory(it) } cfg.workingDirectory.path?.let { cli.withWorkingDirectory(it) }

View file

@ -23,7 +23,7 @@
package com.falsepattern.zigbrains.debugger.runner.base package com.falsepattern.zigbrains.debugger.runner.base
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PtyCommandLine import com.intellij.execution.configurations.PtyCommandLine
@ -34,7 +34,7 @@ import java.io.File
class ZigDebugEmitBinaryInstaller<ProfileState: ZigProfileState<*>>( class ZigDebugEmitBinaryInstaller<ProfileState: ZigProfileState<*>>(
private val profileState: ProfileState, private val profileState: ProfileState,
private val toolchain: AbstractZigToolchain, private val toolchain: ZigToolchain,
private val executableFile: File, private val executableFile: File,
private val exeArgs: List<String> private val exeArgs: List<String>
): Installer { ): Installer {

View file

@ -23,7 +23,7 @@
package com.falsepattern.zigbrains.debugger.runner.base package com.falsepattern.zigbrains.debugger.runner.base
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.intellij.util.system.CpuArch import com.intellij.util.system.CpuArch
import com.jetbrains.cidr.ArchitectureType import com.jetbrains.cidr.ArchitectureType
import com.jetbrains.cidr.execution.RunParameters import com.jetbrains.cidr.execution.RunParameters
@ -31,7 +31,7 @@ import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
abstract class ZigDebugParametersBase<ProfileState: ZigProfileState<*>>( abstract class ZigDebugParametersBase<ProfileState: ZigProfileState<*>>(
private val driverConfiguration: DebuggerDriverConfiguration, private val driverConfiguration: DebuggerDriverConfiguration,
protected val toolchain: AbstractZigToolchain, protected val toolchain: ZigToolchain,
protected val profileState: ProfileState protected val profileState: ProfileState
): RunParameters() { ): RunParameters() {
override fun getDebuggerDriverConfiguration(): DebuggerDriverConfiguration { override fun getDebuggerDriverConfiguration(): DebuggerDriverConfiguration {

View file

@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.debugger.runner.base
import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.debugger.ZigDebugBundle
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
import com.intellij.platform.util.progress.withProgressText import com.intellij.platform.util.progress.withProgressText
@ -39,7 +39,7 @@ import kotlin.io.path.isExecutable
abstract class ZigDebugParametersEmitBinaryBase<ProfileState: ZigProfileState<*>>( abstract class ZigDebugParametersEmitBinaryBase<ProfileState: ZigProfileState<*>>(
driverConfiguration: DebuggerDriverConfiguration, driverConfiguration: DebuggerDriverConfiguration,
toolchain: AbstractZigToolchain, toolchain: ZigToolchain,
profileState: ProfileState, profileState: ProfileState,
) : ZigDebugParametersBase<ProfileState>(driverConfiguration, toolchain, profileState), PreLaunchAware { ) : ZigDebugParametersBase<ProfileState>(driverConfiguration, toolchain, profileState), PreLaunchAware {
@Volatile @Volatile

View file

@ -26,7 +26,7 @@ import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProv
import com.falsepattern.zigbrains.debugger.ZigLocalDebugProcess import com.falsepattern.zigbrains.debugger.ZigLocalDebugProcess
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.run.ZigProgramRunner import com.falsepattern.zigbrains.project.run.ZigProgramRunner
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.intellij.execution.DefaultExecutionResult import com.intellij.execution.DefaultExecutionResult
@ -52,7 +52,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
override suspend fun execute( override suspend fun execute(
state: ProfileState, state: ProfileState,
toolchain: AbstractZigToolchain, toolchain: ZigToolchain,
environment: ExecutionEnvironment environment: ExecutionEnvironment
): RunContentDescriptor? { ): RunContentDescriptor? {
val project = environment.project val project = environment.project
@ -67,7 +67,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
private suspend fun executeWithDriver( private suspend fun executeWithDriver(
state: ProfileState, state: ProfileState,
toolchain: AbstractZigToolchain, toolchain: ZigToolchain,
environment: ExecutionEnvironment, environment: ExecutionEnvironment,
debuggerDriver: DebuggerDriverConfiguration debuggerDriver: DebuggerDriverConfiguration
): RunContentDescriptor? { ): RunContentDescriptor? {
@ -113,7 +113,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
protected abstract fun getDebugParameters( protected abstract fun getDebugParameters(
state: ProfileState, state: ProfileState,
debuggerDriver: DebuggerDriverConfiguration, debuggerDriver: DebuggerDriverConfiguration,
toolchain: AbstractZigToolchain toolchain: ZigToolchain
): ZigDebugParametersBase<ProfileState> ): ZigDebugParametersBase<ProfileState>
private class SharedConsoleBuilder(private val console: ConsoleView) : TextConsoleBuilder() { private class SharedConsoleBuilder(private val console: ConsoleView) : TextConsoleBuilder() {

View file

@ -26,13 +26,13 @@ import com.falsepattern.zigbrains.debugger.ZigDebugBundle
import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinary import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinary
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.Installer
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
class ZigDebugParametersBinary @Throws(ExecutionException::class) constructor(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateBinary) : class ZigDebugParametersBinary @Throws(ExecutionException::class) constructor(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateBinary) :
ZigDebugParametersBase<ZigProfileStateBinary>(driverConfiguration, toolchain, profileState) { ZigDebugParametersBase<ZigProfileStateBinary>(driverConfiguration, toolchain, profileState) {
private val executableFile = profileState.configuration.exePath.path?.toFile() ?: throw ExecutionException(ZigDebugBundle.message("exception.missing-exe-path")) private val executableFile = profileState.configuration.exePath.path?.toFile() ?: throw ExecutionException(ZigDebugBundle.message("exception.missing-exe-path"))
override fun getInstaller(): Installer { override fun getInstaller(): Installer {

View file

@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinar
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfile
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
@ -36,7 +36,7 @@ class ZigDebugRunnerBinary: ZigDebugRunnerBase<ZigProfileStateBinary>() {
override fun getDebugParameters( override fun getDebugParameters(
state: ZigProfileStateBinary, state: ZigProfileStateBinary,
debuggerDriver: DebuggerDriverConfiguration, debuggerDriver: DebuggerDriverConfiguration,
toolchain: AbstractZigToolchain toolchain: ZigToolchain
): ZigDebugParametersBase<ZigProfileStateBinary> { ): ZigDebugParametersBase<ZigProfileStateBinary> {
return ZigDebugParametersBinary(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) return ZigDebugParametersBinary(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
} }

View file

@ -28,7 +28,7 @@ import com.falsepattern.zigbrains.debugger.runner.base.PreLaunchProcessListener
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase
import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.platform.util.progress.withProgressText import com.intellij.platform.util.progress.withProgressText
@ -46,7 +46,7 @@ import kotlin.io.path.isRegularFile
class ZigDebugParametersBuild( class ZigDebugParametersBuild(
driverConfiguration: DebuggerDriverConfiguration, driverConfiguration: DebuggerDriverConfiguration,
toolchain: AbstractZigToolchain, toolchain: ZigToolchain,
profileState: ZigProfileStateBuild profileState: ZigProfileStateBuild
) : ZigDebugParametersBase<ZigProfileStateBuild>(driverConfiguration, toolchain, profileState), PreLaunchAware { ) : ZigDebugParametersBase<ZigProfileStateBuild>(driverConfiguration, toolchain, profileState), PreLaunchAware {
@Volatile @Volatile

View file

@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.execution.build.ZigExecConfigBuild import com.falsepattern.zigbrains.project.execution.build.ZigExecConfigBuild
import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfile
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
@ -36,7 +36,7 @@ class ZigDebugRunnerBuild: ZigDebugRunnerBase<ZigProfileStateBuild>() {
override fun getDebugParameters( override fun getDebugParameters(
state: ZigProfileStateBuild, state: ZigProfileStateBuild,
debuggerDriver: DebuggerDriverConfiguration, debuggerDriver: DebuggerDriverConfiguration,
toolchain: AbstractZigToolchain toolchain: ZigToolchain
): ZigDebugParametersBase<ZigProfileStateBuild> { ): ZigDebugParametersBase<ZigProfileStateBuild> {
return ZigDebugParametersBuild(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) return ZigDebugParametersBuild(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
} }

View file

@ -25,11 +25,11 @@ package com.falsepattern.zigbrains.debugger.runner.run
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersEmitBinaryBase import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersEmitBinaryBase
import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.Installer
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateRun) : class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateRun) :
ZigDebugParametersEmitBinaryBase<ZigProfileStateRun>(driverConfiguration, toolchain, profileState) { ZigDebugParametersEmitBinaryBase<ZigProfileStateRun>(driverConfiguration, toolchain, profileState) {
override fun getInstaller(): Installer { override fun getInstaller(): Installer {
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit()) return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit())

View file

@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.execution.run.ZigExecConfigRun import com.falsepattern.zigbrains.project.execution.run.ZigExecConfigRun
import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfile
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
@ -36,7 +36,7 @@ class ZigDebugRunnerRun: ZigDebugRunnerBase<ZigProfileStateRun>() {
override fun getDebugParameters( override fun getDebugParameters(
state: ZigProfileStateRun, state: ZigProfileStateRun,
debuggerDriver: DebuggerDriverConfiguration, debuggerDriver: DebuggerDriverConfiguration,
toolchain: AbstractZigToolchain toolchain: ZigToolchain
): ZigDebugParametersBase<ZigProfileStateRun> { ): ZigDebugParametersBase<ZigProfileStateRun> {
return ZigDebugParametersRun(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) return ZigDebugParametersRun(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
} }

View file

@ -25,11 +25,11 @@ package com.falsepattern.zigbrains.debugger.runner.test
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersEmitBinaryBase import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersEmitBinaryBase
import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.jetbrains.cidr.execution.Installer import com.jetbrains.cidr.execution.Installer
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
class ZigDebugParametersTest(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateTest) : class ZigDebugParametersTest(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateTest) :
ZigDebugParametersEmitBinaryBase<ZigProfileStateTest>(driverConfiguration, toolchain, profileState) { ZigDebugParametersEmitBinaryBase<ZigProfileStateTest>(driverConfiguration, toolchain, profileState) {
override fun getInstaller(): Installer { override fun getInstaller(): Installer {
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, listOf()) return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, listOf())

View file

@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.execution.test.ZigExecConfigTest import com.falsepattern.zigbrains.project.execution.test.ZigExecConfigTest
import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfile
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
@ -36,7 +36,7 @@ class ZigDebugRunnerTest: ZigDebugRunnerBase<ZigProfileStateTest>() {
override fun getDebugParameters( override fun getDebugParameters(
state: ZigProfileStateTest, state: ZigProfileStateTest,
debuggerDriver: DebuggerDriverConfiguration, debuggerDriver: DebuggerDriverConfiguration,
toolchain: AbstractZigToolchain toolchain: ZigToolchain
): ZigDebugParametersBase<ZigProfileStateTest> { ): ZigDebugParametersBase<ZigProfileStateTest> {
return ZigDebugParametersTest(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state) return ZigDebugParametersTest(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
} }

View file

@ -23,6 +23,7 @@
package com.falsepattern.zigbrains.debugger.toolchain package com.falsepattern.zigbrains.debugger.toolchain
import com.falsepattern.zigbrains.debugger.ZigDebugBundle import com.falsepattern.zigbrains.debugger.ZigDebugBundle
import com.falsepattern.zigbrains.shared.Unarchiver
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.PathManager
@ -34,13 +35,13 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogBuilder import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.util.progress.reportSequentialProgress
import com.intellij.ui.BrowserHyperlinkListener import com.intellij.ui.BrowserHyperlinkListener
import com.intellij.ui.HyperlinkLabel import com.intellij.ui.HyperlinkLabel
import com.intellij.ui.components.JBPanel import com.intellij.ui.components.JBPanel
import com.intellij.util.application import com.intellij.util.application
import com.intellij.util.concurrency.annotations.RequiresEdt import com.intellij.util.concurrency.annotations.RequiresEdt
import com.intellij.util.download.DownloadableFileService import com.intellij.util.download.DownloadableFileService
import com.intellij.util.io.Decompressor
import com.intellij.util.system.CpuArch import com.intellij.util.system.CpuArch
import com.intellij.util.system.OS import com.intellij.util.system.OS
import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager
@ -168,7 +169,9 @@ class ZigDebuggerToolchainService {
} }
try { try {
withContext(Dispatchers.IO) {
downloadAndUnArchive(baseDir, downloadableBinaries) downloadAndUnArchive(baseDir, downloadableBinaries)
}
return DownloadResult.Ok(baseDir) return DownloadResult.Ok(baseDir)
} catch (e: IOException) { } catch (e: IOException) {
//TODO logging //TODO logging
@ -207,6 +210,7 @@ class ZigDebuggerToolchainService {
@Throws(IOException::class) @Throws(IOException::class)
@RequiresEdt @RequiresEdt
private suspend fun downloadAndUnArchive(baseDir: Path, binariesToDownload: List<DownloadableDebuggerBinary>) { private suspend fun downloadAndUnArchive(baseDir: Path, binariesToDownload: List<DownloadableDebuggerBinary>) {
reportSequentialProgress { reporter ->
val service = DownloadableFileService.getInstance() val service = DownloadableFileService.getInstance()
val downloadDir = baseDir.toFile() val downloadDir = baseDir.toFile()
@ -218,7 +222,7 @@ class ZigDebuggerToolchainService {
val downloader = service.createDownloader(descriptions, "Debugger downloading") val downloader = service.createDownloader(descriptions, "Debugger downloading")
val downloadDirectory = downloadPath().toFile() val downloadDirectory = downloadPath().toFile()
val downloadResults = withContext(Dispatchers.IO) { val downloadResults = reporter.sizedStep(100) {
coroutineToIndicator { coroutineToIndicator {
downloader.download(downloadDirectory) downloader.download(downloadDirectory)
} }
@ -229,13 +233,18 @@ class ZigDebuggerToolchainService {
val binaryToDownload = binariesToDownload.first { it.url == downloadUrl } val binaryToDownload = binariesToDownload.first { it.url == downloadUrl }
val propertyName = binaryToDownload.propertyName val propertyName = binaryToDownload.propertyName
val archiveFile = result.first val archiveFile = result.first
reporter.indeterminateStep {
coroutineToIndicator {
Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix) Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix)
}
}
archiveFile.delete() archiveFile.delete()
versions[propertyName] = binaryToDownload.version versions[propertyName] = binaryToDownload.version
} }
saveVersionsFile(baseDir, versions) saveVersionsFile(baseDir, versions)
} }
}
private fun lldbUrls(): Pair<URL, URL>? { private fun lldbUrls(): Pair<URL, URL>? {
val lldb = UrlProvider.lldb(OS.CURRENT, CpuArch.CURRENT) ?: return null val lldb = UrlProvider.lldb(OS.CURRENT, CpuArch.CURRENT) ?: return null
@ -329,38 +338,6 @@ class ZigDebuggerToolchainService {
} }
} }
private enum class Unarchiver {
ZIP {
override val extension = "zip"
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
},
TAR {
override val extension = "tar.gz"
override fun createDecompressor(file: Path) = Decompressor.Tar(file)
},
VSIX {
override val extension = "vsix"
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
};
protected abstract val extension: String
protected abstract fun createDecompressor(file: Path): Decompressor
companion object {
@Throws(IOException::class)
suspend fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) {
runInterruptible {
val unarchiver = entries.find { archivePath.name.endsWith(it.extension) } ?: error("Unexpected archive type: $archivePath")
val dec = unarchiver.createDecompressor(archivePath)
if (prefix != null) {
dec.removePrefixPath(prefix)
}
dec.extract(dst)
}
}
}
}
sealed class DownloadResult { sealed class DownloadResult {
class Ok(val baseDir: Path): DownloadResult() class Ok(val baseDir: Path): DownloadResult()
data object NoUrls: DownloadResult() data object NoUrls: DownloadResult()

View file

@ -22,10 +22,6 @@
package com.falsepattern.zigbrains package com.falsepattern.zigbrains
import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider
import com.intellij.ide.BrowserUtil import com.intellij.ide.BrowserUtil
import com.intellij.ide.plugins.PluginManager import com.intellij.ide.plugins.PluginManager
import com.intellij.notification.Notification import com.intellij.notification.Notification
@ -37,10 +33,8 @@ import com.intellij.openapi.options.Configurable
import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.UserDataHolderBase
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import java.lang.reflect.Method import java.lang.reflect.Method
import kotlin.io.path.pathString
class ZBStartup: ProjectActivity { class ZBStartup: ProjectActivity {
var firstInit = true var firstInit = true
@ -73,19 +67,6 @@ class ZBStartup: ProjectActivity {
notif.notify(null) notif.notify(null)
} }
} }
//Autodetection
val zigProjectState = project.zigProjectSettings.state
if (zigProjectState.toolchainPath.isNullOrBlank()) {
val data = UserDataHolderBase()
data.putUserData(LocalZigToolchain.DIRENV_KEY,
DirenvCmd.direnvInstalled() && !project.isDefault && zigProjectState.direnv
)
val tc = ZigToolchainProvider.suggestToolchain(project, data) ?: return
if (tc is LocalZigToolchain) {
zigProjectState.toolchainPath = tc.location.pathString
project.zigProjectSettings.state = zigProjectState
}
}
} }
} }

View file

@ -29,23 +29,53 @@ import com.intellij.ide.impl.isTrusted
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
import com.intellij.notification.Notifications import com.intellij.notification.Notifications
import com.intellij.openapi.components.*
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.UserDataHolder
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.util.progress.withProgressText import com.intellij.platform.util.progress.withProgressText
import com.intellij.util.io.awaitExit import com.intellij.util.io.awaitExit
import com.intellij.util.xmlb.annotations.Attribute
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.isRegularFile
object DirenvCmd { @Service(Service.Level.PROJECT)
suspend fun importDirenv(project: Project): Env { @State(
if (!direnvInstalled() || !project.isTrusted()) name = "Direnv",
return emptyEnv storages = [Storage("zigbrains.xml")]
val workDir = project.guessProjectDir()?.toNioPath() ?: return emptyEnv )
class DirenvService(val project: Project): SerializablePersistentStateComponent<DirenvService.State>(State()), IDirenvService {
private val mutex = Mutex()
val runOutput = run(project, workDir, "export", "json") 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
val runOutput = run(workDir, "export", "json")
if (runOutput.error) { if (runOutput.error) {
if (runOutput.output.contains("is blocked")) { if (runOutput.output.contains("is blocked")) {
Notifications.Bus.notify(Notification( Notifications.Bus.notify(Notification(
@ -54,7 +84,7 @@ object DirenvCmd {
ZigBrainsBundle.message("notification.content.direnv-blocked"), ZigBrainsBundle.message("notification.content.direnv-blocked"),
NotificationType.ERROR NotificationType.ERROR
)) ))
return emptyEnv return Env.empty
} else { } else {
Notifications.Bus.notify(Notification( Notifications.Bus.notify(Notification(
GROUP_DISPLAY_ID, GROUP_DISPLAY_ID,
@ -62,22 +92,22 @@ object DirenvCmd {
ZigBrainsBundle.message("notification.content.direnv-error", runOutput.output), ZigBrainsBundle.message("notification.content.direnv-error", runOutput.output),
NotificationType.ERROR NotificationType.ERROR
)) ))
return emptyEnv return Env.empty
} }
} }
return if (runOutput.output.isBlank()) { return if (runOutput.output.isBlank()) {
emptyEnv Env.empty
} else { } else {
Env(Json.decodeFromString<Map<String, String>>(runOutput.output)) Env(Json.decodeFromString<Map<String, String>>(runOutput.output))
} }
} }
private suspend fun run(project: Project, workDir: Path, vararg args: String): DirenvOutput { private suspend fun run(workDir: Path, vararg args: String): DirenvOutput {
val cli = GeneralCommandLine("direnv", *args).withWorkingDirectory(workDir) val cli = GeneralCommandLine("direnv", *args).withWorkingDirectory(workDir)
val (process, exitCode) = withProgressText("Running ${cli.commandLineString}") { val (process, exitCode) = withProgressText("Running ${cli.commandLineString}") {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
project.direnvService.mutex.withLock { mutex.withLock {
val process = cli.createProcess() val process = cli.createProcess()
val exitCode = process.awaitExit() val exitCode = process.awaitExit()
process to exitCode process to exitCode
@ -94,17 +124,39 @@ object DirenvCmd {
return DirenvOutput(stdOut, false) return DirenvOutput(stdOut, false)
} }
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" private const val GROUP_DISPLAY_ID = "zigbrains-direnv"
fun getInstance(project: Project): IDirenvService = project.service<DirenvService>()
private val _direnvInstalled by lazy { private val STATE_KEY = Key.create<DirenvState>("DIRENV_STATE")
// Using the builtin stuff here instead of Env because it should only scan for direnv on the process path
PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("direnv") != null fun getStateFor(data: UserDataHolder?, project: Project?): DirenvState {
} return data?.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled
fun direnvInstalled() = _direnvInstalled
} }
suspend fun Project?.getDirenv(): Env { fun setStateFor(data: UserDataHolder, state: DirenvState) {
if (this == null) data.putUserData(STATE_KEY, state)
return emptyEnv
return DirenvCmd.importDirenv(this)
} }
}
}
sealed interface IDirenvService {
val isInstalled: Boolean
val isEnabled: DirenvState
suspend fun import(): Env
}
private val envFiles = listOf(".envrc", ".env")

View file

@ -22,14 +22,19 @@
package com.falsepattern.zigbrains.direnv package com.falsepattern.zigbrains.direnv
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import kotlinx.coroutines.sync.Mutex
@Service(Service.Level.PROJECT) enum class DirenvState {
class DirenvProjectService { Auto,
val mutex = Mutex() Enabled,
Disabled;
fun isEnabled(project: Project?): Boolean {
return when(this) {
Enabled -> true
Disabled -> false
Auto -> project?.service<DirenvService>()?.hasDotEnv() == true
}
}
} }
val Project.direnvService get() = service<DirenvProjectService>()

View file

@ -25,8 +25,10 @@ package com.falsepattern.zigbrains.direnv
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.EnvironmentUtil import com.intellij.util.EnvironmentUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
import java.io.File import java.io.File
import kotlin.io.path.absolute import kotlin.io.path.absolute
@ -34,14 +36,13 @@ import kotlin.io.path.isDirectory
import kotlin.io.path.isExecutable import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile import kotlin.io.path.isRegularFile
@JvmRecord
data class Env(val env: Map<String, String>) { data class Env(val env: Map<String, String>) {
private val path get() = getVariable("PATH")?.split(File.pathSeparatorChar) private val path get() = getVariable("PATH")?.split(File.pathSeparatorChar)
private fun getVariable(name: @NonNls String) = private fun getVariable(name: @NonNls String) =
env.getOrElse(name) { EnvironmentUtil.getValue(name) } env.getOrElse(name) { EnvironmentUtil.getValue(name) }
suspend fun findExecutableOnPATH(exe: @NonNls String) = findAllExecutablesOnPATH(exe).firstOrNull()
fun findAllExecutablesOnPATH(exe: @NonNls String) = flow { fun findAllExecutablesOnPATH(exe: @NonNls String) = flow {
val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe
val paths = path ?: return@flow val paths = path ?: return@flow
@ -54,7 +55,9 @@ data class Env(val env: Map<String, String>) {
continue continue
emit(exePath) emit(exePath)
} }
} }.flowOn(Dispatchers.IO)
}
val emptyEnv = Env(emptyMap()) companion object {
val empty = Env(emptyMap())
}
}

View file

@ -0,0 +1,101 @@
/*
* 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.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY
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
if (sharedState != null) {
it.addItemListener { e ->
if (e.stateChange != ItemEvent.SELECTED)
return@addItemListener
val item = e.item
if (item !is DirenvState)
return@addItemListener
DirenvService.setStateFor(sharedState, item)
}
}
}
}
}
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 DirenvService.getInstance(context).isEnabled
}
override fun setEnabled(context: Project, value: DirenvState) {
context.service<DirenvService>().isEnabledRaw = value
}
}
class Provider: ZigProjectConfigurationProvider {
override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
if (sharedState.getUserData(PROJECT_KEY)?.isDefault != false) {
return null
}
DirenvService.setStateFor(sharedState, DirenvState.Auto)
return ForProject(sharedState)
}
override val index: Int
get() = 100
}
}

View file

@ -24,15 +24,12 @@ package com.falsepattern.zigbrains.project.execution.base
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.ZigConfigurable.ZigConfigModule import com.falsepattern.zigbrains.project.execution.base.ZigConfigurable.ZigConfigModule
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.shared.cli.translateCommandline import com.falsepattern.zigbrains.shared.cli.translateCommandline
import com.falsepattern.zigbrains.shared.element.* import com.falsepattern.zigbrains.shared.element.*
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.options.SettingsEditor import com.intellij.openapi.options.SettingsEditor
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.TextBrowseFolderListener
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBCheckBox

View file

@ -22,9 +22,7 @@
package com.falsepattern.zigbrains.project.execution.base package com.falsepattern.zigbrains.project.execution.base
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory import com.intellij.execution.configurations.ConfigurationFactory
@ -65,8 +63,9 @@ abstract class ZigExecConfig<T: ZigExecConfig<T>>(project: Project, factory: Con
suspend fun patchCommandLine(commandLine: GeneralCommandLine): GeneralCommandLine { suspend fun patchCommandLine(commandLine: GeneralCommandLine): GeneralCommandLine {
if (project.zigProjectSettings.state.direnv) { val direnv = DirenvService.getInstance(project)
commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) if (direnv.isEnabled.isEnabled(project)) {
commandLine.withEnvironment(direnv.import().env)
} }
return commandLine return commandLine
} }

View file

@ -24,28 +24,19 @@ package com.falsepattern.zigbrains.project.execution.base
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.ZigConsoleBuilder import com.falsepattern.zigbrains.project.execution.ZigConsoleBuilder
import com.falsepattern.zigbrains.project.run.ZigProcessHandler import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess
import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking
import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ipc
import com.intellij.build.BuildTextConsoleView
import com.intellij.execution.DefaultExecutionResult
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.CommandLineState import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PtyCommandLine import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.filters.TextConsoleBuilder
import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.project.Project
import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.terminal.TerminalExecutionConsole import com.intellij.platform.util.progress.reportProgress
import com.intellij.util.system.OS import com.intellij.platform.util.progress.reportRawProgress
import kotlin.collections.contains
import kotlin.io.path.pathString import kotlin.io.path.pathString
abstract class ZigProfileState<T: ZigExecConfig<T>> ( abstract class ZigProfileState<T: ZigExecConfig<T>> (
@ -66,12 +57,12 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
suspend fun startProcessSuspend(): ProcessHandler { suspend fun startProcessSuspend(): ProcessHandler {
val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain")) val toolchain = ZigToolchainService.getInstance(environment.project).toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain"))
return getCommandLine(toolchain, false).startIPCAwareProcess(environment.project, emulateTerminal = true) return getCommandLine(toolchain, false).startIPCAwareProcess(environment.project, emulateTerminal = true)
} }
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
open suspend fun getCommandLine(toolchain: AbstractZigToolchain, debug: Boolean): GeneralCommandLine { open suspend fun getCommandLine(toolchain: ZigToolchain, debug: Boolean): GeneralCommandLine {
val workingDir = configuration.workingDirectory val workingDir = configuration.workingDirectory
val zigExePath = toolchain.zig.path() val zigExePath = toolchain.zig.path()

View file

@ -50,7 +50,7 @@ class ZigModuleBuilder: ModuleBuilder() {
override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?): ModuleWizardStep? { override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?): ModuleWizardStep? {
val step = ZigModuleWizardStep(parentDisposable) val step = ZigModuleWizardStep(parentDisposable)
parentDisposable?.let { Disposer.register(it, step.peer) } parentDisposable?.let { Disposer.register(it) { step.peer.dispose() } }
return step return step
} }
@ -65,14 +65,14 @@ class ZigModuleBuilder: ModuleBuilder() {
} }
inner class ZigModuleWizardStep(parent: Disposable?): ModuleWizardStep() { inner class ZigModuleWizardStep(parent: Disposable?): ModuleWizardStep() {
internal val peer = ZigProjectGeneratorPeer(true).also { Disposer.register(parent ?: return@also, it) } internal val peer = ZigProjectGeneratorPeer(true).also { Disposer.register(parent ?: return@also) {it.dispose()} }
override fun getComponent(): JComponent { override fun getComponent(): JComponent {
return peer.myComponent.withBorder() return peer.myComponent.withBorder()
} }
override fun disposeUIResources() { override fun disposeUIResources() {
Disposer.dispose(peer) Disposer.dispose(peer.newProjectPanel)
} }
override fun updateDataModel() { override fun updateDataModel() {

View file

@ -39,9 +39,9 @@ import com.intellij.util.ui.JBUI
import javax.swing.JList import javax.swing.JList
import javax.swing.ListSelectionModel import javax.swing.ListSelectionModel
class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProjectConfigurationProvider.SettingsPanelHolder { class ZigNewProjectPanel(private var handleGit: Boolean): Disposable {
private val git = JBCheckBox() private val git = JBCheckBox()
override val panels = ZigProjectConfigurationProvider.createNewProjectSettingsPanels(this).onEach { Disposer.register(this, it) } val panels = ZigProjectConfigurationProvider.createPanels(null).onEach { Disposer.register(this, it) }
private val templateList = JBList(JBList.createDefaultListModel(defaultTemplates)).apply { private val templateList = JBList(JBList.createDefaultListModel(defaultTemplates)).apply {
selectionMode = ListSelectionModel.SINGLE_SELECTION selectionMode = ListSelectionModel.SINGLE_SELECTION
selectedIndex = 0 selectedIndex = 0
@ -64,7 +64,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject
fun getData(): ZigProjectConfigurationData { fun getData(): ZigProjectConfigurationData {
val selectedTemplate = templateList.selectedValue val selectedTemplate = templateList.selectedValue
return ZigProjectConfigurationData(handleGit && git.isSelected, panels.map { it.data }, selectedTemplate) return ZigProjectConfigurationData(handleGit && git.isSelected, panels, selectedTemplate)
} }
fun attach(p: Panel): Unit = with(p) { fun attach(p: Panel): Unit = with(p) {
@ -73,6 +73,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject
cell(git) cell(git)
} }
} }
panels.filter { it.newProjectBeforeInitSelector }.forEach { it.attach(p) }
group("Zig Project Template") { group("Zig Project Template") {
row { row {
resizableRow() resizableRow()
@ -81,7 +82,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject
.align(AlignY.FILL) .align(AlignY.FILL)
} }
} }
panels.forEach { it.attach(p) } panels.filter { !it.newProjectBeforeInitSelector }.forEach { it.attach(p) }
} }
override fun dispose() { override fun dispose() {

View file

@ -22,9 +22,10 @@
package com.falsepattern.zigbrains.project.newproject package com.falsepattern.zigbrains.project.newproject
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.template.ZigInitTemplate import com.falsepattern.zigbrains.project.template.ZigInitTemplate
import com.falsepattern.zigbrains.project.template.ZigProjectTemplate import com.falsepattern.zigbrains.project.template.ZigProjectTemplate
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
@ -42,7 +43,7 @@ import kotlinx.coroutines.launch
@JvmRecord @JvmRecord
data class ZigProjectConfigurationData( data class ZigProjectConfigurationData(
val git: Boolean, val git: Boolean,
val conf: List<ZigProjectConfigurationProvider.Settings>, val conf: List<SubConfigurable<Project>>,
val selectedTemplate: ZigProjectTemplate val selectedTemplate: ZigProjectTemplate
) { ) {
@RequiresBackgroundThread @RequiresBackgroundThread
@ -54,9 +55,7 @@ data class ZigProjectConfigurationData(
if (!reporter.indeterminateStep("Initializing project") { if (!reporter.indeterminateStep("Initializing project") {
if (template is ZigInitTemplate) { if (template is ZigInitTemplate) {
val toolchain = conf val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run {
.mapNotNull { it as? ZigProjectConfigurationProvider.ToolchainProvider }
.firstNotNullOfOrNull { it.toolchain } ?: run {
Notification( Notification(
"zigbrains", "zigbrains",
"Tried to generate project with zig init, but zig toolchain is invalid", "Tried to generate project with zig init, but zig toolchain is invalid",

View file

@ -31,9 +31,9 @@ import com.intellij.platform.ProjectGeneratorPeer
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.panel
import javax.swing.JComponent import javax.swing.JComponent
class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigProjectConfigurationData>, Disposable { class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigProjectConfigurationData> {
private val newProjectPanel by lazy { val newProjectPanel by lazy {
ZigNewProjectPanel(handleGit).also { Disposer.register(this, it) } ZigNewProjectPanel(handleGit)
} }
val myComponent: JComponent by lazy { val myComponent: JComponent by lazy {
panel { panel {
@ -61,6 +61,7 @@ class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigP
return false return false
} }
override fun dispose() { fun dispose() {
newProjectPanel.dispose()
} }
} }

View file

@ -23,8 +23,8 @@
package com.falsepattern.zigbrains.project.run package com.falsepattern.zigbrains.project.run
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.RunProfileState import com.intellij.execution.configurations.RunProfileState
@ -61,7 +61,7 @@ abstract class ZigProgramRunner<ProfileState : ZigProfileState<*>>(protected val
val state = castProfileState(baseState) ?: return null val state = castProfileState(baseState) ?: return null
val toolchain = environment.project.zigProjectSettings.state.toolchain ?: run { val toolchain = ZigToolchainService.getInstance(environment.project).toolchain ?: run {
Notification( Notification(
"zigbrains", "zigbrains",
"Zig project toolchain not set, cannot execute program! Please configure it in [Settings | Languages & Frameworks | Zig]", "Zig project toolchain not set, cannot execute program! Please configure it in [Settings | Languages & Frameworks | Zig]",
@ -81,5 +81,5 @@ abstract class ZigProgramRunner<ProfileState : ZigProfileState<*>>(protected val
protected abstract fun castProfileState(state: ZigProfileState<*>): ProfileState? protected abstract fun castProfileState(state: ZigProfileState<*>): ProfileState?
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
abstract suspend fun execute(state: ProfileState, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? abstract suspend fun execute(state: ProfileState, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor?
} }

View file

@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.project.run
import com.falsepattern.zigbrains.project.execution.base.ZigExecConfig import com.falsepattern.zigbrains.project.execution.base.ZigExecConfig
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.intellij.execution.configurations.RunProfile import com.intellij.execution.configurations.RunProfile
import com.intellij.execution.executors.DefaultRunExecutor import com.intellij.execution.executors.DefaultRunExecutor
@ -32,10 +32,13 @@ import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.RunContentBuilder import com.intellij.execution.runners.RunContentBuilder
import com.intellij.execution.ui.RunContentDescriptor import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.blockingContext
class ZigRegularRunner: ZigProgramRunner<ZigProfileState<*>>(DefaultRunExecutor.EXECUTOR_ID) { class ZigRegularRunner: ZigProgramRunner<ZigProfileState<*>>(DefaultRunExecutor.EXECUTOR_ID) {
override suspend fun execute(state: ZigProfileState<*>, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? { override suspend fun execute(state: ZigProfileState<*>, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? {
val exec = state.execute(environment.executor, this) val exec = blockingContext {
state.execute(environment.executor, this)
}
return withEDTContext(ModalityState.any()) { return withEDTContext(ModalityState.any()) {
val runContentBuilder = RunContentBuilder(exec, environment) val runContentBuilder = RunContentBuilder(exec, environment)
runContentBuilder.showRunContent(null) runContentBuilder.showRunContent(null)

View file

@ -22,11 +22,14 @@
package com.falsepattern.zigbrains.project.settings package com.falsepattern.zigbrains.project.settings
import com.falsepattern.zigbrains.shared.MultiConfigurable import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
class ZigConfigurable(project: Project): MultiConfigurable(ZigProjectConfigurationProvider.createConfigurables(project)) { class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter<Project>() {
override fun getDisplayName(): String { override fun instantiate(): List<SubConfigurable<Project>> {
return "Zig" return ZigProjectConfigurationProvider.createPanels(context)
} }
override fun getDisplayName() = ZigBrainsBundle.message("settings.project.display-name")
} }

View file

@ -1,60 +0,0 @@
/*
* 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.project.settings
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.dsl.builder.Panel
class ZigProjectConfigurable(private val project: Project): SubConfigurable {
private var settingsPanel: ZigProjectSettingsPanel? = null
override fun createComponent(holder: ZigProjectConfigurationProvider.SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel {
settingsPanel?.let { Disposer.dispose(it) }
val sp = ZigProjectSettingsPanel(holder, project).apply { attach(panel) }.also { Disposer.register(this, it) }
settingsPanel = sp
return sp
}
override fun isModified(): Boolean {
return project.zigProjectSettings.isModified(settingsPanel?.data ?: return false)
}
override fun apply() {
val service = project.zigProjectSettings
val data = settingsPanel?.data ?: return
val modified = service.isModified(data)
service.state = data
if (modified) {
ZigProjectConfigurationProvider.mainConfigChanged(project)
}
}
override fun reset() {
settingsPanel?.data = project.zigProjectSettings.state
}
override fun dispose() {
settingsPanel = null
}
}

View file

@ -22,42 +22,56 @@
package com.falsepattern.zigbrains.project.settings package com.falsepattern.zigbrains.project.settings
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.Disposable
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.ui.dsl.builder.Panel import com.intellij.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.UserDataHolderBase
interface ZigProjectConfigurationProvider { interface ZigProjectConfigurationProvider {
fun handleMainConfigChanged(project: Project) fun create(sharedState: IUserDataBridge): SubConfigurable<Project>?
fun createConfigurable(project: Project): SubConfigurable val index: Int
fun createNewProjectSettingsPanel(holder: SettingsPanelHolder): SettingsPanel?
val priority: Int
companion object { companion object {
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigProjectConfigurationProvider>("com.falsepattern.zigbrains.projectConfigProvider") private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigProjectConfigurationProvider>("com.falsepattern.zigbrains.projectConfigProvider")
fun mainConfigChanged(project: Project) { val PROJECT_KEY: Key<Project> = Key.create("Project")
EXTENSION_POINT_NAME.extensionList.forEach { it.handleMainConfigChanged(project) } fun createPanels(project: Project?): List<SubConfigurable<Project>> {
} val sharedState = UserDataBridge()
fun createConfigurables(project: Project): List<SubConfigurable> { sharedState.putUserData(PROJECT_KEY, project)
return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.map { it.createConfigurable(project) } return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(sharedState) }
}
fun createNewProjectSettingsPanels(holder: SettingsPanelHolder): List<SettingsPanel> {
return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.mapNotNull { it.createNewProjectSettingsPanel(holder) }
} }
} }
interface SettingsPanel: Disposable {
val data: Settings interface IUserDataBridge: UserDataHolder {
fun attach(p: Panel) fun addUserDataChangeListener(listener: UserDataListener)
fun direnvChanged(state: Boolean) 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)
} }
interface SettingsPanelHolder {
val panels: List<SettingsPanel>
} }
interface Settings {
fun apply(project: Project)
}
interface ToolchainProvider {
val toolchain: AbstractZigToolchain?
} }
} }

View file

@ -1,55 +0,0 @@
/*
* 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.project.settings
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.xmlb.annotations.Transient
import kotlin.io.path.isDirectory
import kotlin.io.path.pathString
data class ZigProjectSettings(
var direnv: Boolean = false,
var overrideStdPath: Boolean = false,
var explicitPathToStd: String? = null,
var toolchainPath: String? = null
): ZigProjectConfigurationProvider.Settings, ZigProjectConfigurationProvider.ToolchainProvider {
override fun apply(project: Project) {
project.zigProjectSettings.loadState(this)
}
@get:Transient
@set:Transient
override var toolchain: LocalZigToolchain?
get() {
val nioPath = toolchainPath?.toNioPathOrNull() ?: return null
if (!nioPath.isDirectory()) {
return null
}
return LocalZigToolchain(nioPath)
}
set(value) {
toolchainPath = value?.location?.pathString
}
}

View file

@ -1,60 +0,0 @@
/*
* 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.project.settings
import com.falsepattern.zigbrains.project.toolchain.stdlib.ZigSyntheticLibrary
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project
import kotlinx.coroutines.launch
@Service(Service.Level.PROJECT)
@State(
name = "ZigProjectSettings",
storages = [Storage("zigbrains.xml")]
)
class ZigProjectSettingsService(val project: Project): PersistentStateComponent<ZigProjectSettings> {
@Volatile
private var state = ZigProjectSettings()
override fun getState(): ZigProjectSettings {
return state.copy()
}
fun setState(value: ZigProjectSettings) {
this.state = value
zigCoroutineScope.launch {
ZigSyntheticLibrary.reload(project, value)
}
}
override fun loadState(state: ZigProjectSettings) {
setState(state)
}
fun isModified(otherData: ZigProjectSettings): Boolean {
return state != otherData
}
}
val Project.zigProjectSettings get() = service<ZigProjectSettingsService>()

View file

@ -20,7 +20,7 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.toolchain.stdlib package com.falsepattern.zigbrains.project.stdlib
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.AdditionalLibraryRootsProvider import com.intellij.openapi.roots.AdditionalLibraryRootsProvider

View file

@ -20,41 +20,43 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.toolchain.stdlib package com.falsepattern.zigbrains.project.stdlib
import com.falsepattern.zigbrains.Icons import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.project.settings.ZigProjectSettings import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.intellij.navigation.ItemPresentation import com.intellij.navigation.ItemPresentation
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.roots.SyntheticLibrary import com.intellij.openapi.roots.SyntheticLibrary
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.refreshAndFindVirtualDirectory import com.intellij.openapi.vfs.refreshAndFindVirtualDirectory
import com.intellij.platform.backend.workspace.WorkspaceModel import com.intellij.platform.backend.workspace.WorkspaceModel
import com.intellij.platform.backend.workspace.toVirtualFileUrl import com.intellij.platform.backend.workspace.toVirtualFileUrl
import com.intellij.platform.workspace.jps.entities.* import com.intellij.platform.workspace.jps.entities.*
import com.intellij.project.isDirectoryBased
import com.intellij.project.stateStore
import com.intellij.workspaceModel.ide.legacyBridge.LegacyBridgeJpsEntitySourceFactory import com.intellij.workspaceModel.ide.legacyBridge.LegacyBridgeJpsEntitySourceFactory
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.* import java.util.*
import javax.swing.Icon import javax.swing.Icon
class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresentation { class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresentation {
private var state: ZigProjectSettings = project.zigProjectSettings.state.copy() private var toolchain: ZigToolchain? = ZigToolchainService.getInstance(project).toolchain
private val roots by lazy { private val roots by lazy {
runBlocking {getRoot(state, project)}?.let { setOf(it) } ?: emptySet() runBlocking {getRoot(toolchain, project)}?.let { setOf(it) } ?: emptySet()
} }
private val name by lazy { private val name by lazy {
getName(state, project) getName(toolchain, project)
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (other !is ZigSyntheticLibrary) if (other !is ZigSyntheticLibrary)
return false return false
return state == other.state return toolchain == other.toolchain
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -75,15 +77,23 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen
companion object { companion object {
private const val ZIG_LIBRARY_ID = "Zig SDK" private const val ZIG_LIBRARY_ID = "Zig SDK"
private const val ZIG_MODULE_ID = "Zig" private const val ZIG_MODULE_ID = "ZigBrains"
suspend fun reload(project: Project, state: ZigProjectSettings) { suspend fun reload(project: Project, toolchain: ZigToolchain?) {
val moduleId = ModuleId(ZIG_MODULE_ID) val moduleId = ModuleId(ZIG_MODULE_ID)
val workspaceModel = WorkspaceModel.getInstance(project) val workspaceModel = WorkspaceModel.getInstance(project)
val root = getRoot(state, project) ?: return val root = getRoot(toolchain, project) ?: return
val libRoot = LibraryRoot(root.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()), LibraryRootTypeId.SOURCES) val libRoot = LibraryRoot(root.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()), LibraryRootTypeId.SOURCES)
val libraryTableId = LibraryTableId.ProjectLibraryTableId val libraryTableId = LibraryTableId.ProjectLibraryTableId
val libraryId = LibraryId(ZIG_LIBRARY_ID, libraryTableId) val libraryId = LibraryId(ZIG_LIBRARY_ID, libraryTableId)
val baseModuleDir = project.guessProjectDir()?.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()) ?: return
var baseModuleDirFile: VirtualFile? = null
if (project.isDirectoryBased) {
baseModuleDirFile = project.stateStore.directoryStorePath?.refreshAndFindVirtualDirectory()
}
if (baseModuleDirFile == null) {
baseModuleDirFile = project.guessProjectDir()
}
val baseModuleDir = baseModuleDirFile?.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()) ?: return
workspaceModel.update("Update Zig std") { builder -> workspaceModel.update("Update Zig std") { builder ->
builder.resolve(moduleId)?.let { moduleEntity -> builder.resolve(moduleId)?.let { moduleEntity ->
builder.removeEntity(moduleEntity) builder.removeEntity(moduleEntity)
@ -118,37 +128,39 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen
} }
private fun getName( private fun getName(
state: ZigProjectSettings, toolchain: ZigToolchain?,
project: Project project: Project
): String { ): String {
val tc = state.toolchain ?: return "Zig" val tc = toolchain ?: return "Zig"
val version = runBlocking { tc.zig.getEnv(project) }.mapCatching { it.version }.getOrElse { return "Zig" } toolchain.name?.let { return it }
return "Zig $version" runBlocking { tc.zig.getEnv(project) }
.mapCatching { it.version }
.getOrNull()
?.let { return "Zig $it" }
return "Zig"
} }
suspend fun getRoot( suspend fun getRoot(
state: ZigProjectSettings, toolchain: ZigToolchain?,
project: Project project: Project
): VirtualFile? { ): VirtualFile? {
val toolchain = state.toolchain //TODO universal
if (state.overrideStdPath) run { if (toolchain !is LocalZigToolchain) {
val ePathStr = state.explicitPathToStd ?: return@run return null
val ePath = ePathStr.toNioPathOrNull() ?: return@run }
if (toolchain.std != null) run {
val ePath = toolchain.std
if (ePath.isAbsolute) { if (ePath.isAbsolute) {
val roots = ePath.refreshAndFindVirtualDirectory() ?: return@run val roots = ePath.refreshAndFindVirtualDirectory() ?: return@run
return roots return roots
} else if (toolchain != null) { }
val stdPath = toolchain.location.resolve(ePath) val stdPath = toolchain.location.resolve(ePath)
if (stdPath.isAbsolute) { if (stdPath.isAbsolute) {
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run
return roots return roots
} }
} }
}
if (toolchain != null) {
val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null
return roots return roots
} }
return null
}

View file

@ -22,8 +22,8 @@
package com.falsepattern.zigbrains.project.steps.discovery package com.falsepattern.zigbrains.project.steps.discovery
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.project.steps.discovery.ZigStepDiscoveryListener.ErrorType import com.falsepattern.zigbrains.project.steps.discovery.ZigStepDiscoveryListener.ErrorType
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
@ -76,7 +76,7 @@ class ZigStepDiscoveryService(private val project: Project) {
private tailrec suspend fun doReload() { private tailrec suspend fun doReload() {
preReload() preReload()
val toolchain = project.zigProjectSettings.state.toolchain ?: run { val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run {
errorReload(ErrorType.MissingToolchain) errorReload(ErrorType.MissingToolchain)
return return
} }

View file

@ -1,66 +0,0 @@
/*
* 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.project.toolchain
import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.io.toNioPathOrNull
import kotlinx.serialization.json.*
import kotlin.io.path.pathString
class LocalZigToolchainProvider: ZigToolchainProvider<LocalZigToolchain> {
override suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): LocalZigToolchain? {
val env = if (project != null && (extraData.getUserData(LocalZigToolchain.DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) {
DirenvCmd.importDirenv(project)
} else {
emptyEnv
}
val zigExePath = env.findExecutableOnPATH("zig") ?: return null
return LocalZigToolchain(zigExePath.parent)
}
override val serialMarker: String
get() = "local"
override fun deserialize(data: JsonElement): LocalZigToolchain? {
if (data !is JsonObject)
return null
val loc = data["location"] as? JsonPrimitive ?: return null
val path = loc.content.toNioPathOrNull() ?: return null
return LocalZigToolchain(path)
}
override fun canSerialize(toolchain: AbstractZigToolchain): Boolean {
return toolchain is LocalZigToolchain
}
override fun serialize(toolchain: LocalZigToolchain): JsonElement {
return buildJsonObject {
put("location", toolchain.location.pathString)
}
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.project.toolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainListService.MyState
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.resolve
import com.falsepattern.zigbrains.project.toolchain.base.toRef
import com.falsepattern.zigbrains.shared.UUIDMapSerializable
import com.falsepattern.zigbrains.shared.UUIDStorage
import com.intellij.openapi.components.*
@Service(Service.Level.APP)
@State(
name = "ZigToolchainList",
storages = [Storage("zigbrains.xml")]
)
class ZigToolchainListService: UUIDMapSerializable.Converting<ZigToolchain, ZigToolchain.Ref, MyState>(MyState()) {
override fun serialize(value: ZigToolchain) = value.toRef()
override fun deserialize(value: ZigToolchain.Ref) = value.resolve()
override fun getStorage(state: MyState) = state.toolchains
override fun updateStorage(state: MyState, storage: ToolchainStorage) = state.copy(toolchains = storage)
data class MyState(@JvmField val toolchains: ToolchainStorage = emptyMap())
companion object {
@JvmStatic
fun getInstance(): ZigToolchainListService = service<ZigToolchainListService>()
}
}
inline val zigToolchainList: ZigToolchainListService get() = ZigToolchainListService.getInstance()
private typealias ToolchainStorage = UUIDStorage<ZigToolchain.Ref>

View file

@ -1,69 +0,0 @@
/*
* 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.project.toolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider.Companion.EXTENSION_POINT_NAME
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolder
import com.intellij.util.xmlb.Converter
import kotlinx.serialization.json.*
sealed interface ZigToolchainProvider<in T: AbstractZigToolchain> {
suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain?
val serialMarker: String
fun deserialize(data: JsonElement): AbstractZigToolchain?
fun canSerialize(toolchain: AbstractZigToolchain): Boolean
fun serialize(toolchain: T): JsonElement
companion object {
val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainProvider<*>>("com.falsepattern.zigbrains.toolchainProvider")
suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain? {
return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(project, extraData) }
}
}
}
@Suppress("UNCHECKED_CAST")
private fun <T: AbstractZigToolchain> ZigToolchainProvider<T>.serialize(toolchain: AbstractZigToolchain) = serialize(toolchain as T)
class ZigToolchainConverter: Converter<AbstractZigToolchain>() {
override fun fromString(value: String): AbstractZigToolchain? {
val json = Json.parseToJsonElement(value) as? JsonObject ?: return null
val marker = (json["marker"] as? JsonPrimitive)?.contentOrNull ?: return null
val data = json["data"] ?: return null
val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null
return provider.deserialize(data)
}
override fun toString(value: AbstractZigToolchain): String? {
val provider = EXTENSION_POINT_NAME.extensionList.find { it.canSerialize(value) } ?: return null
return buildJsonObject {
put("marker", provider.serialMarker)
put("data", provider.serialize(value))
}.toString()
}
}

View file

@ -0,0 +1,80 @@
/*
* 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.project.toolchain
import com.falsepattern.zigbrains.project.stdlib.ZigSyntheticLibrary
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.asUUID
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.application.EDT
import com.intellij.openapi.components.SerializablePersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.annotations.Attribute
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.UUID
@Service(Service.Level.PROJECT)
@State(
name = "ZigToolchain",
storages = [Storage("zigbrains.xml")]
)
class ZigToolchainService(private val project: Project): SerializablePersistentStateComponent<ZigToolchainService.State>(State()) {
var toolchainUUID: UUID?
get() = state.toolchain.ifBlank { null }?.asUUID()?.takeIf {
if (it in zigToolchainList) {
true
} else {
updateState {
it.copy(toolchain = "")
}
false
}
}
set(value) {
updateState {
it.copy(toolchain = value?.toString() ?: "")
}
zigCoroutineScope.launch(Dispatchers.EDT) {
ZigSyntheticLibrary.reload(project, toolchain)
}
}
val toolchain: ZigToolchain?
get() = toolchainUUID?.let { zigToolchainList[it] }
data class State(
@JvmField
@Attribute
var toolchain: String = ""
)
companion object {
@JvmStatic
fun getInstance(project: Project): ZigToolchainService = project.service<ZigToolchainService>()
}
}

View file

@ -0,0 +1,72 @@
/*
* 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.project.toolchain.base
import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool
import com.falsepattern.zigbrains.shared.NamedObject
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import com.intellij.util.xmlb.annotations.Attribute
import com.intellij.util.xmlb.annotations.MapAnnotation
import java.nio.file.Path
/**
* These MUST be stateless and interchangeable! (e.g., immutable data class)
*/
interface ZigToolchain: NamedObject<ZigToolchain> {
val zig: ZigCompilerTool get() = ZigCompilerTool(this)
val extraData: Map<String, String>
/**
* Returned type must be the same class
*/
fun withExtraData(map: Map<String, String>): ZigToolchain
fun workingDirectory(project: Project? = null): Path?
suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine
fun pathToExecutable(toolName: String, project: Project? = null): Path
data class Ref(
@JvmField
@Attribute
val marker: String? = null,
@JvmField
val data: Map<String, String>? = null,
@JvmField
val extraData: Map<String, String>? = null,
)
}
fun <T: ZigToolchain> T.withExtraData(key: String, value: String?): T {
val newMap = HashMap<String, String>()
newMap.putAll(extraData.filter { (theKey, _) -> theKey != key})
if (value != null) {
newMap[key] = value
}
@Suppress("UNCHECKED_CAST")
return withExtraData(newMap) as T
}

View file

@ -0,0 +1,104 @@
/*
* 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.project.toolchain.base
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.NlsContexts
import com.intellij.ui.dsl.builder.panel
import java.util.UUID
import java.util.function.Supplier
import javax.swing.JComponent
abstract class ZigToolchainConfigurable<T: ZigToolchain>(
val uuid: UUID,
tc: T,
val data: ZigProjectConfigurationProvider.IUserDataBridge?,
val modal: Boolean
): NamedConfigurable<UUID>() {
var toolchain: T = tc
set(value) {
zigToolchainList[uuid] = value
field = value
}
init {
data?.putUserData(TOOLCHAIN_KEY, Supplier{toolchain})
}
private var myViews: List<ImmutableElementPanel<T>> = emptyList()
abstract fun createPanel(): ImmutableElementPanel<T>
override fun createOptionsPanel(): JComponent? {
var views = myViews
if (views.isEmpty()) {
views = ArrayList<ImmutableElementPanel<T>>()
views.add(createPanel())
views.addAll(createZigToolchainExtensionPanels(data, if (modal) PanelState.ModalEditor else PanelState.ListEditor))
myViews = views
}
val p = panel {
views.forEach { it.attach(this@panel) }
}.withMinimumWidth(20)
views.forEach { it.reset(toolchain) }
return p
}
override fun getEditableObject(): UUID? {
return uuid
}
override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? {
return displayName
}
override fun getDisplayName(): @NlsContexts.ConfigurableName String? {
return toolchain.name
}
override fun isModified(): Boolean {
return myViews.any { it.isModified(toolchain) }
}
override fun apply() {
toolchain = myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc }
}
override fun reset() {
myViews.forEach { it.reset(toolchain) }
}
override fun disposeUIResources() {
myViews.forEach { it.dispose() }
myViews = emptyList()
super.disposeUIResources()
}
companion object {
val TOOLCHAIN_KEY: Key<Supplier<ZigToolchain?>> = Key.create("TOOLCHAIN")
}
}

View file

@ -0,0 +1,46 @@
/*
* 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.project.toolchain.base
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.intellij.openapi.extensions.ExtensionPointName
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainExtensionsProvider>("com.falsepattern.zigbrains.toolchainExtensionsProvider")
interface ZigToolchainExtensionsProvider {
fun <T : ZigToolchain> createExtensionPanel(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): ImmutableElementPanel<T>?
val index: Int
}
fun <T: ZigToolchain> createZigToolchainExtensionPanels(sharedState: ZigProjectConfigurationProvider.IUserDataBridge?, state: PanelState): List<ImmutableElementPanel<T>> {
return EXTENSION_POINT_NAME.extensionList.sortedBy{ it.index }.mapNotNull {
it.createExtensionPanel(sharedState, state)
}
}
enum class PanelState {
ProjectEditor,
ListEditor,
ModalEditor
}

View file

@ -0,0 +1,98 @@
/*
* 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.project.toolchain.base
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flowOn
import java.util.UUID
import kotlin.collections.none
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainProvider>("com.falsepattern.zigbrains.toolchainProvider")
internal interface ZigToolchainProvider {
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, data: ZigProjectConfigurationProvider.IUserDataBridge?, modal: Boolean): ZigToolchainConfigurable<*>
suspend fun suggestToolchains(project: Project?, data: UserDataHolder): Flow<ZigToolchain>
fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean)
}
fun ZigToolchain.Ref.resolve(): ZigToolchain? {
val marker = this.marker ?: return null
val data = this.data ?: return null
val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null
return provider.deserialize(data)?.let { tc -> this.extraData?.let { extraData -> tc.withExtraData(extraData) }}
}
fun ZigToolchain.toRef(): ZigToolchain.Ref {
val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException()
return ZigToolchain.Ref(provider.serialMarker, provider.serialize(this), this.extraData)
}
fun ZigToolchain.createNamedConfigurable(uuid: UUID, data: ZigProjectConfigurationProvider.IUserDataBridge?, modal: Boolean): ZigToolchainConfigurable<*> {
val provider = EXTENSION_POINT_NAME.extensionList.find { it.isCompatible(this) } ?: throw IllegalStateException()
return provider.createConfigurable(uuid, this, data, modal)
}
@OptIn(ExperimentalCoroutinesApi::class)
fun suggestZigToolchains(project: Project? = null, data: UserDataHolder = emptyData): Flow<ZigToolchain> {
val existing = zigToolchainList.map { (_, tc) -> tc }
return EXTENSION_POINT_NAME.extensionList.asFlow().flatMapConcat { ext ->
val compatibleExisting = existing.filter { ext.isCompatible(it) }
val suggestions = ext.suggestToolchains(project, data)
suggestions.filter { suggestion ->
compatibleExisting.none { existing -> ext.matchesSuggestion(existing, suggestion) }
}
}.flowOn(Dispatchers.IO)
}
fun ZigToolchain.render(component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: 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

@ -0,0 +1,41 @@
/*
* 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.project.toolchain.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.getSuggestedLocalToolchainPath
import com.falsepattern.zigbrains.shared.downloader.Downloader
import com.falsepattern.zigbrains.shared.downloader.LocalSelector
import com.intellij.openapi.util.NlsContexts
import java.awt.Component
import java.nio.file.Path
class LocalToolchainDownloader(component: Component) : Downloader<LocalZigToolchain, ZigVersionInfo>(component) {
override val windowTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.title")
override val versionInfoFetchTitle get() = ZigBrainsBundle.message("settings.toolchain.downloader.progress.fetch")
override fun downloadProgressTitle(version: ZigVersionInfo) = ZigBrainsBundle.message("settings.toolchain.downloader.progress.install", version.version.rawVersion)
override fun localSelector() = LocalToolchainSelector(component)
override suspend fun downloadVersionList() = ZigVersionInfo.downloadVersionList()
override fun getSuggestedPath() = getSuggestedLocalToolchainPath()
}

View file

@ -0,0 +1,107 @@
/*
* 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.project.toolchain.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.downloader.LocalSelector
import com.falsepattern.zigbrains.shared.withUniqueName
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.util.Disposer
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.ui.components.textFieldWithBrowseButton
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.awt.Component
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString
class LocalToolchainSelector(component: Component): LocalSelector<LocalZigToolchain>(component) {
override val windowTitle: String
get() = ZigBrainsBundle.message("settings.toolchain.local-selector.title")
override val descriptor: FileChooserDescriptor
get() = FileChooserDescriptorFactory.createSingleFolderDescriptor()
.withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title"))
override suspend fun verify(path: Path): VerifyResult {
var tc = resolve(path, null)
var result: VerifyResult
if (tc == null) {
result = VerifyResult(
null,
false,
AllIcons.General.Error,
ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid"),
)
} else {
val existingToolchain = zigToolchainList
.mapNotNull { it.second as? LocalZigToolchain }
.firstOrNull { it.location == tc.location }
if (existingToolchain != null) {
result = VerifyResult(
null,
true,
AllIcons.General.Warning,
existingToolchain.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) }
?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed")
)
} else {
result = VerifyResult(
null,
true,
AllIcons.General.Information,
ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok")
)
}
}
if (tc != null) {
tc = zigToolchainList.withUniqueName(tc)
}
return result.copy(name = tc?.name)
}
override suspend fun resolve(path: Path, name: String?): LocalZigToolchain? {
return runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) {
LocalZigToolchain.tryFromPath(path)?.let { it.withName(name ?: it.name) }
} }.getOrNull()
}
}

View file

@ -0,0 +1,114 @@
/*
* 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.project.toolchain.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.Unarchiver
import com.falsepattern.zigbrains.shared.downloader.VersionInfo
import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball
import com.falsepattern.zigbrains.shared.downloader.downloadTarball
import com.falsepattern.zigbrains.shared.downloader.flattenDownloadDir
import com.falsepattern.zigbrains.shared.downloader.getTarballIfCompatible
import com.falsepattern.zigbrains.shared.downloader.tempPluginDir
import com.falsepattern.zigbrains.shared.downloader.unpackTarball
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.util.progress.*
import com.intellij.util.asSafely
import com.intellij.util.download.DownloadableFileService
import com.intellij.util.io.createDirectories
import com.intellij.util.io.delete
import com.intellij.util.io.move
import com.intellij.util.system.CpuArch
import com.intellij.util.system.OS
import com.intellij.util.text.SemVer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.decodeFromStream
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively
import kotlin.io.path.isDirectory
import kotlin.io.path.name
@JvmRecord
data class ZigVersionInfo(
override val version: SemVer,
override val date: String,
val docs: String,
val notes: String,
val src: Tarball?,
override val dist: Tarball
): VersionInfo {
companion object {
@OptIn(ExperimentalSerializationApi::class)
suspend fun downloadVersionList(): List<ZigVersionInfo> {
return withContext(Dispatchers.IO) {
val service = DownloadableFileService.getInstance()
val tempFile = FileUtil.createTempFile(tempPluginDir, "index", ".json", false, false)
val desc = service.createFileDescription("https://ziglang.org/download/index.json", tempFile.name)
val downloader = service.createDownloader(listOf(desc), ZigBrainsBundle.message("settings.toolchain.downloader.service.index"))
val downloadResults = coroutineToIndicator {
downloader.download(tempPluginDir)
}
if (downloadResults.isEmpty())
return@withContext emptyList()
val index = downloadResults[0].first
val info = index.inputStream().use { Json.decodeFromStream<JsonObject>(it) }
index.delete()
return@withContext info.mapNotNull { (version, data) -> parseVersion(version, data) }.toList()
}
}
}
}
private fun parseVersion(versionKey: String, data: JsonElement): ZigVersionInfo? {
if (data !is JsonObject)
return null
val versionTag = data["version"]?.asSafely<JsonPrimitive>()?.content
val version = SemVer.parseFromText(versionTag) ?: SemVer.parseFromText(versionKey)
?: return null
val date = data["date"]?.asSafely<JsonPrimitive>()?.content ?: ""
val docs = data["docs"]?.asSafely<JsonPrimitive>()?.content ?: ""
val notes = data["notes"]?.asSafely<JsonPrimitive>()?.content ?: ""
val src = data["src"]?.asSafely<JsonObject>()?.let { Json.decodeFromJsonElement<Tarball>(it) }
val dist = data.firstNotNullOfOrNull { (dist, tb) -> getTarballIfCompatible(dist, tb) }
?: return null
return ZigVersionInfo(version, date, docs, notes, src, dist)
}

View file

@ -20,27 +20,30 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.toolchain package com.falsepattern.zigbrains.project.toolchain.local
import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.util.KeyWithDefaultValue import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.util.keyFMap.KeyFMap
import java.nio.file.Path import java.nio.file.Path
class LocalZigToolchain(val location: Path): AbstractZigToolchain() { @JvmRecord
data class LocalZigToolchain(val location: Path, val std: Path? = null, override val name: String? = null, override val extraData: Map<String, String> = emptyMap()): ZigToolchain {
override fun workingDirectory(project: Project?): Path? { override fun workingDirectory(project: Project?): Path? {
return project?.guessProjectDir()?.toNioPathOrNull() return project?.guessProjectDir()?.toNioPathOrNull()
} }
override suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project?): GeneralCommandLine { override suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project?): GeneralCommandLine {
if (project != null && (commandLine.getUserData(DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) { if (project != null && DirenvService.getStateFor(commandLine, project).isEnabled(project)) {
commandLine.withEnvironment(DirenvCmd.importDirenv(project).env) commandLine.withEnvironment(DirenvService.getInstance(project).import().env)
} }
return commandLine return commandLine
} }
@ -50,11 +53,17 @@ class LocalZigToolchain(val location: Path): AbstractZigToolchain() {
return location.resolve(exeName) return location.resolve(exeName)
} }
companion object { override fun withExtraData(map: Map<String, String>): ZigToolchain {
val DIRENV_KEY = KeyWithDefaultValue.create<Boolean>("ZIG_LOCAL_DIRENV") return this.copy(extraData = map)
}
override fun withName(newName: String?): LocalZigToolchain {
return this.copy(name = newName)
}
companion object {
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
fun ensureLocal(toolchain: AbstractZigToolchain): LocalZigToolchain { fun ensureLocal(toolchain: ZigToolchain): LocalZigToolchain {
if (toolchain is LocalZigToolchain) { if (toolchain is LocalZigToolchain) {
return toolchain return toolchain
} else { } else {
@ -62,5 +71,20 @@ class LocalZigToolchain(val location: Path): AbstractZigToolchain() {
throw ExecutionException("The debugger only supports local zig toolchain") throw ExecutionException("The debugger only supports local zig toolchain")
} }
} }
suspend fun tryFromPath(path: Path): LocalZigToolchain? {
var tc = LocalZigToolchain(path)
if (!tc.zig.fileValid()) {
return null
}
val versionStr = tc.zig
.getEnv(null)
.getOrNull()
?.version
if (versionStr != null) {
tc = tc.copy(name = "Zig $versionStr")
}
return tc
}
} }
} }

View file

@ -20,28 +20,21 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.lsp package com.falsepattern.zigbrains.project.toolchain.local
import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsConfigurable
import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsPanel
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.intellij.openapi.project.Project import java.util.UUID
import com.intellij.openapi.project.ProjectManager
class ZLSProjectConfigurationProvider: ZigProjectConfigurationProvider { class LocalZigToolchainConfigurable(
override fun handleMainConfigChanged(project: Project) { uuid: UUID,
startLSP(project, true) toolchain: LocalZigToolchain,
} data: ZigProjectConfigurationProvider.IUserDataBridge?,
modal: Boolean
): ZigToolchainConfigurable<LocalZigToolchain>(uuid, toolchain, data, modal) {
override fun createPanel() = LocalZigToolchainPanel()
override fun createConfigurable(project: Project): SubConfigurable { override fun setDisplayName(name: String?) {
return ZLSSettingsConfigurable(project) toolchain = toolchain.copy(name = name)
} }
override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel {
return ZLSSettingsPanel(ProjectManager.getInstance().defaultProject)
}
override val priority: Int
get() = 1000
} }

View file

@ -20,24 +20,16 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.settings package com.falsepattern.zigbrains.project.toolchain.local
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.application.ModalityState import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.ui.DocumentAdapter import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBColor import com.intellij.ui.JBColor
import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBCheckBox
@ -52,12 +44,9 @@ import kotlinx.coroutines.launch
import javax.swing.event.DocumentEvent import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString import kotlin.io.path.pathString
class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvider.SettingsPanelHolder, private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel { class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase<LocalZigToolchain>() {
private val direnv = JBCheckBox(ZigBrainsBundle.message("settings.project.label.direnv")).apply { addActionListener {
dispatchDirenvUpdate()
} }
private val pathToToolchain = textFieldWithBrowseButton( private val pathToToolchain = textFieldWithBrowseButton(
project, null,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain")) FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
).also { ).also {
it.textField.document.addDocumentListener(object : DocumentAdapter() { it.textField.document.addDocumentListener(object : DocumentAdapter() {
@ -68,7 +57,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide
Disposer.register(this, it) Disposer.register(this, it)
} }
private val toolchainVersion = JBTextArea().also { it.isEditable = false } private val toolchainVersion = JBTextArea().also { it.isEditable = false }
private val stdFieldOverride = JBCheckBox(ZigBrainsBundle.message("settings.project.label.override-std")).apply { private val stdFieldOverride = JBCheckBox().apply {
addChangeListener { addChangeListener {
if (isSelected) { if (isSelected) {
pathToStd.isEnabled = true pathToStd.isEnabled = true
@ -79,92 +68,57 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide
} }
} }
private val pathToStd = textFieldWithBrowseButton( private val pathToStd = textFieldWithBrowseButton(
project, null,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-std")) FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-std"))
).also { Disposer.register(this, it) } ).also { Disposer.register(this, it) }
private var debounce: Job? = null private var debounce: Job? = null
private fun dispatchDirenvUpdate() {
holder.panels.forEach {
it.direnvChanged(direnv.isSelected)
}
}
override fun direnvChanged(state: Boolean) {
dispatchAutodetect(true)
}
private fun dispatchAutodetect(force: Boolean) {
project.zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) {
withModalProgress(ModalTaskOwner.component(pathToToolchain), "Detecting Zig...", TaskCancellation.cancellable()) {
autodetect(force)
}
}
}
suspend fun autodetect(force: Boolean) {
if (!force && pathToToolchain.text.isNotBlank())
return
val data = UserDataHolderBase()
data.putUserData(LocalZigToolchain.DIRENV_KEY, !project.isDefault && direnv.isSelected && DirenvCmd.direnvInstalled())
val tc = ZigToolchainProvider.suggestToolchain(project, data) ?: return
if (tc !is LocalZigToolchain) {
TODO("Implement non-local zig toolchain in config")
}
if (force || pathToToolchain.text.isBlank()) {
pathToToolchain.text = tc.location.pathString
dispatchUpdateUI()
}
}
override var data
get() = ZigProjectSettings(
direnv.isSelected,
stdFieldOverride.isSelected,
pathToStd.text.ifBlank { null },
pathToToolchain.text.ifBlank { null }
)
set(value) {
direnv.isSelected = value.direnv
pathToToolchain.text = value.toolchainPath ?: ""
stdFieldOverride.isSelected = value.overrideStdPath
pathToStd.text = value.explicitPathToStd ?: ""
pathToStd.isEnabled = value.overrideStdPath
dispatchUpdateUI()
}
override fun attach(p: Panel): Unit = with(p) { override fun attach(p: Panel): Unit = with(p) {
data = project.zigProjectSettings.state super.attach(p)
if (project.isDefault) { row(ZigBrainsBundle.message("settings.toolchain.local.path.label")) {
row(ZigBrainsBundle.message("settings.project.label.toolchain")) {
cell(pathToToolchain).resizableColumn().align(AlignX.FILL) cell(pathToToolchain).resizableColumn().align(AlignX.FILL)
} }
row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) { row(ZigBrainsBundle.message("settings.toolchain.local.version.label")) {
cell(toolchainVersion) cell(toolchainVersion)
} }
} else { row(ZigBrainsBundle.message("settings.toolchain.local.std.label")) {
group(ZigBrainsBundle.message("settings.project.group.title")) {
row(ZigBrainsBundle.message("settings.project.label.toolchain")) {
cell(pathToToolchain).resizableColumn().align(AlignX.FILL)
if (DirenvCmd.direnvInstalled()) {
cell(direnv)
}
}
row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) {
cell(toolchainVersion)
}
row(ZigBrainsBundle.message("settings.project.label.std-location")) {
cell(pathToStd).resizableColumn().align(AlignX.FILL)
cell(stdFieldOverride) cell(stdFieldOverride)
cell(pathToStd).resizableColumn().align(AlignX.FILL)
} }
} }
override fun isModified(toolchain: LocalZigToolchain): Boolean {
val name = nameFieldValue ?: return false
val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return false
val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null
return name != toolchain.name || toolchain.location != location || toolchain.std != std
}
override fun apply(toolchain: LocalZigToolchain): LocalZigToolchain? {
val location = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull() ?: return null
val std = if (stdFieldOverride.isSelected) pathToStd.text.ifBlank { null }?.toNioPathOrNull() else null
return toolchain.copy(location = location, std = std, name = nameFieldValue ?: "")
}
override fun reset(toolchain: LocalZigToolchain?) {
nameFieldValue = toolchain?.name ?: ""
this.pathToToolchain.text = toolchain?.location?.pathString ?: ""
val std = toolchain?.std
if (std != null) {
stdFieldOverride.isSelected = true
pathToStd.text = std.pathString
pathToStd.isEnabled = true
} else {
stdFieldOverride.isSelected = false
pathToStd.text = ""
pathToStd.isEnabled = false
dispatchUpdateUI()
} }
dispatchAutodetect(false)
} }
private fun dispatchUpdateUI() { private fun dispatchUpdateUI() {
debounce?.cancel("New debounce") debounce?.cancel("New debounce")
debounce = project.zigCoroutineScope.launch { debounce = zigCoroutineScope.launch {
updateUI() updateUI()
} }
} }
@ -183,7 +137,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide
} }
val toolchain = LocalZigToolchain(pathToToolchain) val toolchain = LocalZigToolchain(pathToToolchain)
val zig = toolchain.zig val zig = toolchain.zig
val env = zig.getEnv(project).getOrElse { throwable -> val env = zig.getEnv(null).getOrElse { throwable ->
throwable.printStackTrace() throwable.printStackTrace()
withEDTContext(ModalityState.any()) { withEDTContext(ModalityState.any()) {
toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}" toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}"
@ -194,7 +148,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide
return return
} }
val version = env.version val version = env.version
val stdPath = env.stdPath(toolchain, project) val stdPath = env.stdPath(toolchain, null)
withEDTContext(ModalityState.any()) { withEDTContext(ModalityState.any()) {
toolchainVersion.text = version toolchainVersion.text = version

View file

@ -0,0 +1,156 @@
/*
* 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.project.toolchain.local
import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.direnv.Env
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
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.ui.renderPathNameComponent
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.io.FileUtil
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.system.OS
import kotlinx.coroutines.ExperimentalCoroutinesApi
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.mapNotNull
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID
import kotlin.io.path.isDirectory
import kotlin.io.path.pathString
class LocalZigToolchainProvider: ZigToolchainProvider {
override val serialMarker: String
get() = "local"
override fun deserialize(data: Map<String, String>): ZigToolchain? {
val location = data["location"]?.toNioPathOrNull() ?: return null
val std = data["std"]?.toNioPathOrNull()
val name = data["name"]
return LocalZigToolchain(location, std, name)
}
override fun isCompatible(toolchain: ZigToolchain): Boolean {
return toolchain is LocalZigToolchain
}
override fun serialize(toolchain: ZigToolchain): Map<String, String> {
toolchain as LocalZigToolchain
val map = HashMap<String, String>()
toolchain.location.pathString.let { map["location"] = it }
toolchain.std?.pathString?.let { map["std"] = it }
toolchain.name?.let { map["name"] = it }
return map
}
override fun matchesSuggestion(
toolchain: ZigToolchain,
suggestion: ZigToolchain
): Boolean {
toolchain as LocalZigToolchain
suggestion as LocalZigToolchain
return toolchain.location == suggestion.location
}
override fun createConfigurable(
uuid: UUID,
toolchain: ZigToolchain,
data: ZigProjectConfigurationProvider.IUserDataBridge?,
modal: Boolean
): ZigToolchainConfigurable<*> {
toolchain as LocalZigToolchain
return LocalZigToolchainConfigurable(uuid, toolchain, data, modal)
}
@OptIn(ExperimentalCoroutinesApi::class)
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 ->
runCatching {
Files.newDirectoryStream(dir).use { stream ->
stream.toList().filterNotNull().asFlow()
}
}.getOrElse { emptyFlow() }
}
val joined = flowOf(pathToolchains, wellKnown).flattenConcat()
return joined.mapNotNull { LocalZigToolchain.tryFromPath(it) }
}
override fun render(toolchain: ZigToolchain, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) {
toolchain as LocalZigToolchain
val name = toolchain.name
val path = toolchain.location.pathString
renderPathNameComponent(path, name, "Zig", component, isSuggestion, isSelected)
}
}
fun getSuggestedLocalToolchainPath(): Path? {
return getWellKnown().getOrNull(0)
}
/**
* Returns the paths to the following list of folders:
*
* 1. DATA/zig
* 2. DATA/zigup
* 3. HOME/.zig
*
* Where DATA is:
* - ~/Library on macOS
* - %LOCALAPPDATA% on Windows
* - $XDG_DATA_HOME (or ~/.local/share if not set) on other OSes
*
* and HOME is the user home path
*/
private fun getWellKnown(): List<Path> {
val home = System.getProperty("user.home")?.toNioPathOrNull() ?: return emptyList()
val xdgDataHome = when(OS.CURRENT) {
OS.macOS -> home.resolve("Library")
OS.Windows -> System.getenv("LOCALAPPDATA")?.toNioPathOrNull()
else -> System.getenv("XDG_DATA_HOME")?.toNioPathOrNull() ?: home.resolve(Path.of(".local", "share"))
}
val res = ArrayList<Path>()
if (xdgDataHome != null && xdgDataHome.isDirectory()) {
res.add(xdgDataHome.resolve("zig"))
res.add(xdgDataHome.resolve("zigup"))
}
res.add(home.resolve(".zig"))
return res
}

View file

@ -22,14 +22,14 @@
package com.falsepattern.zigbrains.project.toolchain.tools package com.falsepattern.zigbrains.project.toolchain.tools
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSerializable
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.nio.file.Path import java.nio.file.Path
class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) { class ZigCompilerTool(toolchain: ZigToolchain) : ZigTool(toolchain) {
override val toolName: String override val toolName: String
get() = "zig" get() = "zig"

View file

@ -22,15 +22,16 @@
package com.falsepattern.zigbrains.project.toolchain.tools package com.falsepattern.zigbrains.project.toolchain.tools
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.cli.call import com.falsepattern.zigbrains.shared.cli.call
import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.isRegularFile
abstract class ZigTool(val toolchain: AbstractZigToolchain) { abstract class ZigTool(val toolchain: ZigToolchain) {
abstract val toolName: String abstract val toolName: String
suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result<ProcessOutput> { suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result<ProcessOutput> {
@ -38,6 +39,11 @@ abstract class ZigTool(val toolchain: AbstractZigToolchain) {
return cli.call(timeoutMillis, ipcProject = ipcProject) return cli.call(timeoutMillis, ipcProject = ipcProject)
} }
fun fileValid(): Boolean {
val exe = toolchain.pathToExecutable(toolName)
return exe.isRegularFile()
}
private suspend fun createBaseCommandLine( private suspend fun createBaseCommandLine(
workingDirectory: Path?, workingDirectory: Path?,
vararg parameters: String vararg parameters: String

View file

@ -19,15 +19,16 @@
* You should have received a copy of the GNU Lesser General Public License * You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.toolchain
package com.falsepattern.zigbrains.project.toolchain.tools
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.nio.file.Path import java.nio.file.Path
@JvmRecord @JvmRecord
@Serializable @Serializable
data class ZigToolchainEnvironmentSerializable( data class ZigToolchainEnvironmentSerializable(

View file

@ -0,0 +1,36 @@
/*
* 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.project.toolchain.ui
import com.intellij.openapi.Disposable
import com.intellij.ui.dsl.builder.Panel
interface ImmutableElementPanel<T>: Disposable {
fun attach(p: Panel)
fun isModified(elem: T): Boolean
/**
* Returned object must be the exact same class as the provided one.
*/
fun apply(elem: T): T?
fun reset(elem: T?)
}

View file

@ -0,0 +1,44 @@
/*
* 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.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.NamedObject
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
abstract class ImmutableNamedElementPanelBase<T>: ImmutableElementPanel<T> {
private val nameField = JBTextField(25)
protected var nameFieldValue: String?
get() = nameField.text.ifBlank { null }
set(value) {nameField.text = value ?: ""}
override fun attach(p: Panel): Unit = with(p) {
row(ZigBrainsBundle.message("settings.toolchain.base.name.label")) {
cell(nameField).resizableColumn().align(AlignX.FILL)
}
separator()
}
}

View file

@ -0,0 +1,42 @@
/*
* 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.project.toolchain.ui
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.downloader.LocalToolchainDownloader
import com.falsepattern.zigbrains.project.toolchain.downloader.LocalToolchainSelector
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.withUniqueName
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread
import java.awt.Component
import java.util.UUID
internal object ZigToolchainComboBoxHandler {
@RequiresBackgroundThread
suspend fun onItemSelected(context: Component, elem: ListElem.Pseudo<ZigToolchain>): UUID? = when(elem) {
is ListElem.One.Suggested -> zigToolchainList.withUniqueName(elem.instance)
is ListElem.Download -> LocalToolchainDownloader(context).download()
is ListElem.FromDisk -> LocalToolchainSelector(context).browse()
}?.let { zigToolchainList.registerNew(it) }
}

View file

@ -0,0 +1,101 @@
/*
* 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.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.createNamedConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.suggestZigToolchains
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.ui.ListElemIn
import com.falsepattern.zigbrains.shared.ui.Separator
import com.falsepattern.zigbrains.shared.ui.UUIDComboBoxDriver
import com.falsepattern.zigbrains.shared.ui.ZBComboBox
import com.falsepattern.zigbrains.shared.ui.ZBContext
import com.falsepattern.zigbrains.shared.ui.ZBModel
import com.falsepattern.zigbrains.shared.ui.asActual
import com.falsepattern.zigbrains.shared.ui.asPending
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.UserDataHolder
import java.awt.Component
import java.util.UUID
sealed interface ZigToolchainDriver: UUIDComboBoxDriver<ZigToolchain> {
override val theMap get() = zigToolchainList
override fun createContext(model: ZBModel<ZigToolchain>): ZBContext<ZigToolchain> {
return TCContext(null, model)
}
override fun createComboBox(model: ZBModel<ZigToolchain>): ZBComboBox<ZigToolchain> {
return TCComboBox(model)
}
override suspend fun resolvePseudo(
context: Component,
elem: ListElem.Pseudo<ZigToolchain>
): UUID? {
return ZigToolchainComboBoxHandler.onItemSelected(context, elem)
}
object ForList: ZigToolchainDriver {
override suspend fun constructModelList(): List<ListElemIn<ZigToolchain>> {
val modelList = ArrayList<ListElemIn<ZigToolchain>>()
modelList.addAll(ListElem.fetchGroup())
modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true))
modelList.add(suggestZigToolchains().asPending())
return modelList
}
override fun createNamedConfigurable(
uuid: UUID,
elem: ZigToolchain
): NamedConfigurable<UUID> {
return elem.createNamedConfigurable(uuid, ZigProjectConfigurationProvider.UserDataBridge(), false)
}
}
class ForSelector(val data: ZigProjectConfigurationProvider.IUserDataBridge): ZigToolchainDriver {
override suspend fun constructModelList(): List<ListElemIn<ZigToolchain>> {
val modelList = ArrayList<ListElemIn<ZigToolchain>>()
modelList.add(ListElem.None())
modelList.addAll(zigToolchainList.map { it.asActual() }.sortedBy { it.instance.name })
modelList.add(Separator("", true))
modelList.addAll(ListElem.fetchGroup())
modelList.add(Separator(ZigBrainsBundle.message("settings.toolchain.model.detected.separator"), true))
modelList.add(suggestZigToolchains(data.getUserData(PROJECT_KEY), data).asPending())
return modelList
}
override fun createNamedConfigurable(
uuid: UUID,
elem: ZigToolchain
): NamedConfigurable<UUID> {
return elem.createNamedConfigurable(uuid, data, true)
}
}
}

View file

@ -0,0 +1,139 @@
/*
* 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.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.PanelState
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.project.toolchain.base.createZigToolchainExtensionPanels
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.falsepattern.zigbrains.shared.ui.UUIDMapSelector
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Key
import com.intellij.ui.dsl.builder.Panel
import kotlinx.coroutines.launch
import java.util.UUID
import java.util.function.Supplier
class ZigToolchainEditor(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge):
UUIDMapSelector<ZigToolchain>(ZigToolchainDriver.ForSelector(sharedState)),
SubConfigurable<Project>,
ZigProjectConfigurationProvider.UserDataListener
{
private var myViews: List<ImmutableElementPanel<ZigToolchain>> = emptyList()
init {
sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }})
sharedState.addUserDataChangeListener(this)
}
override fun onUserDataChanged(key: Key<*>) {
if (key == ZigToolchainConfigurable.TOOLCHAIN_KEY)
return
zigCoroutineScope.launch { listChanged() }
}
override fun attach(p: Panel): Unit = with(p) {
row(ZigBrainsBundle.message(
if (sharedState.getUserData(PROJECT_KEY)?.isDefault == true)
"settings.toolchain.editor.toolchain-default.label"
else
"settings.toolchain.editor.toolchain.label")
) {
attachComboBoxRow(this)
}
var views = myViews
if (views.isEmpty()) {
views = ArrayList<ImmutableElementPanel<ZigToolchain>>()
views.addAll(createZigToolchainExtensionPanels(sharedState, PanelState.ProjectEditor))
myViews = views
}
views.forEach { it.attach(p) }
}
override fun onSelection(uuid: UUID?) {
sharedState.putUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY, Supplier{selectedUUID?.let { zigToolchainList[it] }})
refreshViews(uuid)
}
private fun refreshViews(uuid: UUID?) {
val toolchain = uuid?.let { zigToolchainList[it] }
myViews.forEach { it.reset(toolchain) }
}
override fun isModified(context: Project): Boolean {
val uuid = selectedUUID
if (ZigToolchainService.getInstance(context).toolchainUUID != selectedUUID) {
return true
}
if (uuid == null)
return false
val tc = zigToolchainList[uuid]
if (tc == null)
return false
return myViews.any { it.isModified(tc) }
}
override fun apply(context: Project) {
val uuid = selectedUUID
ZigToolchainService.getInstance(context).toolchainUUID = uuid
if (uuid == null)
return
val tc = zigToolchainList[uuid]
if (tc == null)
return
val finalTc = myViews.fold(tc) { acc, view -> view.apply(acc) ?: acc }
zigToolchainList[uuid] = finalTc
}
override fun reset(context: Project?) {
val project = context ?: ProjectManager.getInstance().defaultProject
val svc = ZigToolchainService.getInstance(project)
val uuid = svc.toolchainUUID
selectedUUID = uuid
refreshViews(uuid)
}
override fun dispose() {
super.dispose()
sharedState.removeUserDataChangeListener(this)
myViews.forEach { it.dispose() }
myViews = emptyList()
}
override val newProjectBeforeInitSelector get() = true
class Provider: ZigProjectConfigurationProvider {
override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
return ZigToolchainEditor(sharedState).also { it.reset(sharedState.getUserData(PROJECT_KEY)) }
}
override val index: Int get() = 0
}
}

View file

@ -0,0 +1,31 @@
/*
* 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.project.toolchain.ui
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor
class ZigToolchainListEditor : UUIDMapEditor<ZigToolchain>(ZigToolchainDriver.ForList) {
override fun getDisplayName() = ZigBrainsBundle.message("settings.toolchain.list.title")
}

View file

@ -0,0 +1,84 @@
/*
* 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.project.toolchain.ui
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.render
import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.ui.ZBCellRenderer
import com.falsepattern.zigbrains.shared.ui.ZBComboBox
import com.falsepattern.zigbrains.shared.ui.ZBContext
import com.falsepattern.zigbrains.shared.ui.ZBModel
import com.intellij.icons.AllIcons
import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.icons.EMPTY_ICON
import javax.swing.JList
class TCComboBox(model: ZBModel<ZigToolchain>): ZBComboBox<ZigToolchain>(model, ::TCCellRenderer)
class TCContext(project: Project?, model: ZBModel<ZigToolchain>): ZBContext<ZigToolchain>(project, model, ::TCCellRenderer)
class TCCellRenderer(getModel: () -> ZBModel<ZigToolchain>): ZBCellRenderer<ZigToolchain>(getModel) {
override fun customizeCellRenderer(
list: JList<out ListElem<ZigToolchain>?>,
value: ListElem<ZigToolchain>?,
index: Int,
selected: Boolean,
hasFocus: Boolean
) {
icon = EMPTY_ICON
when (value) {
is ListElem.One -> {
val (icon, isSuggestion) = when(value) {
is ListElem.One.Suggested -> AllIcons.General.Information to true
is ListElem.One.Actual -> Icons.Zig to false
}
this.icon = icon
val item = value.instance
item.render(this, isSuggestion, index == -1)
}
is ListElem.Download -> {
icon = AllIcons.Actions.Download
append(ZigBrainsBundle.message("settings.toolchain.model.download.text"))
}
is ListElem.FromDisk -> {
icon = AllIcons.General.OpenDisk
append(ZigBrainsBundle.message("settings.toolchain.model.from-disk.text"))
}
is ListElem.Pending -> {
icon = AllIcons.Empty
append(ZigBrainsBundle.message("settings.toolchain.model.loading.text"), SimpleTextAttributes.GRAYED_ATTRIBUTES)
}
is ListElem.None, null -> {
icon = AllIcons.General.BalloonError
append(ZigBrainsBundle.message("settings.toolchain.model.none.text"), SimpleTextAttributes.ERROR_ATTRIBUTES)
}
}
}
}

View file

@ -1,57 +0,0 @@
/*
* 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.shared
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.intellij.openapi.options.Configurable
import com.intellij.openapi.util.Disposer
import com.intellij.ui.dsl.builder.panel
import javax.swing.JComponent
abstract class MultiConfigurable(val configurables: List<SubConfigurable>): Configurable, ZigProjectConfigurationProvider.SettingsPanelHolder {
final override var panels: List<ZigProjectConfigurationProvider.SettingsPanel> = emptyList()
private set
override fun createComponent(): JComponent? {
return panel {
panels = configurables.map { it.createComponent(this@MultiConfigurable, this@panel) }
}
}
override fun isModified(): Boolean {
return configurables.any { it.isModified() }
}
override fun apply() {
configurables.forEach { it.apply() }
}
override fun reset() {
configurables.forEach { it.reset() }
}
override fun disposeUIResources() {
configurables.forEach { Disposer.dispose(it) }
panels = emptyList()
}
}

View file

@ -0,0 +1,32 @@
/*
* 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.shared
interface NamedObject<T: NamedObject<T>> {
val name: String?
/**
* Returned object must be the exact same class as the called one.
*/
fun withName(newName: String?): T
}

View file

@ -22,16 +22,73 @@
package com.falsepattern.zigbrains.shared package com.falsepattern.zigbrains.shared
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.SettingsPanelHolder
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.options.Configurable
import com.intellij.openapi.util.Disposer
import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.panel
import java.util.ArrayList
import javax.swing.JComponent
interface SubConfigurable: Disposable { interface SubConfigurable<T>: Disposable {
fun createComponent(holder: SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel fun attach(panel: Panel)
fun isModified(): Boolean fun isModified(context: T): Boolean
@Throws(ConfigurationException::class) fun apply(context: T)
fun apply() fun reset(context: T?)
fun reset()
val newProjectBeforeInitSelector: Boolean get() = false
abstract class Adapter<T>: Configurable {
private val myConfigurables: MutableList<SubConfigurable<T>> = ArrayList()
abstract fun instantiate(): List<SubConfigurable<T>>
protected abstract val context: T
override fun createComponent(): JComponent? {
val configurables: List<SubConfigurable<T>>
synchronized(myConfigurables) {
if (myConfigurables.isEmpty()) {
disposeConfigurables()
}
configurables = instantiate()
configurables.forEach { it.reset(context) }
myConfigurables.clear()
myConfigurables.addAll(configurables)
}
return panel {
configurables.forEach { it.attach(this) }
}
}
override fun isModified(): Boolean {
synchronized(myConfigurables) {
return myConfigurables.any { it.isModified(context) }
}
}
override fun apply() {
synchronized(myConfigurables) {
myConfigurables.forEach { it.apply(context) }
}
}
override fun reset() {
synchronized(myConfigurables) {
myConfigurables.forEach { it.reset(context) }
}
}
override fun disposeUIResources() {
synchronized(myConfigurables) {
disposeConfigurables()
}
super.disposeUIResources()
}
private fun disposeConfigurables() {
val configurables = ArrayList(myConfigurables)
myConfigurables.clear()
configurables.forEach { Disposer.dispose(it) }
}
}
} }

View file

@ -0,0 +1,28 @@
/*
* 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.shared
import java.util.UUID
fun String.asUUID(): UUID? = UUID.fromString(this)
fun UUID.asString(): String = toString()

View file

@ -0,0 +1,192 @@
/*
* 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.shared
import com.intellij.openapi.components.SerializablePersistentStateComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import java.util.UUID
import kotlin.collections.any
typealias UUIDStorage<T> = Map<String, T>
abstract class UUIDMapSerializable<T, S: Any>(init: S): SerializablePersistentStateComponent<S>(init), ChangeTrackingStorage {
private val changeListeners = ArrayList<WeakReference<StorageChangeListener>>()
protected abstract fun getStorage(state: S): UUIDStorage<T>
protected abstract fun updateStorage(state: S, storage: UUIDStorage<T>): S
override fun addChangeListener(listener: StorageChangeListener) {
synchronized(changeListeners) {
changeListeners.add(WeakReference(listener))
}
}
override fun removeChangeListener(listener: StorageChangeListener) {
synchronized(changeListeners) {
changeListeners.removeIf {
val v = it.get()
v == null || v === listener
}
}
}
protected fun registerNewUUID(value: T): UUID {
var uuid = UUID.randomUUID()
updateState {
val newMap = HashMap<String, T>()
newMap.putAll(getStorage(it))
var uuidStr = uuid.asString()
while (newMap.containsKey(uuidStr)) {
uuid = UUID.randomUUID()
uuidStr = uuid.asString()
}
newMap[uuidStr] = value
updateStorage(it, newMap)
}
notifyChanged()
return uuid
}
protected fun setStateUUID(uuid: UUID, value: T) {
val str = uuid.asString()
updateState {
val newMap = HashMap<String, T>()
newMap.putAll(getStorage(it))
newMap[str] = value
updateStorage(it, newMap)
}
notifyChanged()
}
protected fun getStateUUID(uuid: UUID): T? {
return getStorage(state)[uuid.asString()]
}
protected fun hasStateUUID(uuid: UUID): Boolean {
return getStorage(state).containsKey(uuid.asString())
}
protected fun removeStateUUID(uuid: UUID) {
val str = uuid.asString()
updateState {
updateStorage(state, getStorage(state).filter { it.key != str })
}
notifyChanged()
}
private fun notifyChanged() {
synchronized(changeListeners) {
var i = 0
while (i < changeListeners.size) {
val v = changeListeners[i].get()
if (v == null) {
changeListeners.removeAt(i)
continue
}
zigCoroutineScope.launch {
v()
}
i++
}
}
}
abstract class Converting<R, T, S: Any>(init: S):
UUIDMapSerializable<T, S>(init),
AccessibleStorage<R>,
IterableStorage<R>
{
protected abstract fun serialize(value: R): T
protected abstract fun deserialize(value: T): R?
override fun registerNew(value: R): UUID {
val ser = serialize(value)
return registerNewUUID(ser)
}
override operator fun set(uuid: UUID, value: R) {
val ser = serialize(value)
setStateUUID(uuid, ser)
}
override operator fun get(uuid: UUID): R? {
return getStateUUID(uuid)?.let { deserialize(it) }
}
override operator fun contains(uuid: UUID): Boolean {
return hasStateUUID(uuid)
}
override fun remove(uuid: UUID) {
removeStateUUID(uuid)
}
override fun iterator(): Iterator<Pair<UUID, R>> {
return getStorage(state)
.asSequence()
.mapNotNull {
val uuid = it.key.asUUID() ?: return@mapNotNull null
val tc = deserialize(it.value) ?: return@mapNotNull null
uuid to tc
}.iterator()
}
}
abstract class Direct<T, S: Any>(init: S): Converting<T, T, S>(init) {
override fun serialize(value: T): T {
return value
}
override fun deserialize(value: T): T? {
return value
}
}
}
typealias StorageChangeListener = suspend CoroutineScope.() -> Unit
interface ChangeTrackingStorage {
fun addChangeListener(listener: StorageChangeListener)
fun removeChangeListener(listener: StorageChangeListener)
}
interface AccessibleStorage<R> {
fun registerNew(value: R): UUID
operator fun set(uuid: UUID, value: R)
operator fun get(uuid: UUID): R?
operator fun contains(uuid: UUID): Boolean
fun remove(uuid: UUID)
}
interface IterableStorage<R>: Iterable<Pair<UUID, R>>
fun <R: NamedObject<R>, T: R> IterableStorage<R>.withUniqueName(value: T): T {
val baseName = value.name ?: ""
var index = 0
var currentName = baseName
val names = this.map { (_, existing) -> existing.name }
while (names.any { it == currentName }) {
index++
currentName = "$baseName ($index)"
}
@Suppress("UNCHECKED_CAST")
return value.withName(currentName) as T
}

View file

@ -0,0 +1,73 @@
/*
* 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.shared
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.util.io.Decompressor
import java.io.IOException
import java.nio.file.Path
import kotlin.io.path.name
enum class Unarchiver {
ZIP {
override val extension = "zip"
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
},
TAR_GZ {
override val extension = "tar.gz"
override fun createDecompressor(file: Path) = Decompressor.Tar(file)
},
TAR_XZ {
override val extension = "tar.xz"
override fun createDecompressor(file: Path) = Decompressor.Tar(file)
},
VSIX {
override val extension = "vsix"
override fun createDecompressor(file: Path) = Decompressor.Zip(file)
};
protected abstract val extension: String
protected abstract fun createDecompressor(file: Path): Decompressor
companion object {
@Throws(IOException::class)
fun unarchive(archivePath: Path, dst: Path, prefix: String? = null) {
val unarchiver = entries.find { archivePath.name.endsWith(it.extension) }
?: error("Unexpected archive type: $archivePath")
val dec = unarchiver.createDecompressor(archivePath)
val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
indicator.isIndeterminate = true
indicator.text = "Extracting archive"
dec.filter {
indicator.text2 = it
indicator.checkCanceled()
true
}
if (prefix != null) {
dec.removePrefixPath(prefix)
}
dec.extract(dst)
}
}
}

View file

@ -28,7 +28,6 @@ import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ipc import com.falsepattern.zigbrains.shared.ipc.ipc
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.options.ConfigurationException

View file

@ -30,6 +30,8 @@ import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.runWithModalProgressBlocking import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.util.application import com.intellij.util.application
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.awt.Component
import kotlin.coroutines.CoroutineContext
inline fun <T> runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleFactory: () -> String, cancellationFactory: () -> TaskCancellation = {TaskCancellation.cancellable()}, noinline action: suspend CoroutineScope.() -> T): T { inline fun <T> runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleFactory: () -> String, cancellationFactory: () -> TaskCancellation = {TaskCancellation.cancellable()}, noinline action: suspend CoroutineScope.() -> T): T {
return if (application.isDispatchThread) { return if (application.isDispatchThread) {
@ -40,7 +42,11 @@ inline fun <T> runModalOrBlocking(taskOwnerFactory: () -> ModalTaskOwner, titleF
} }
suspend inline fun <T> withEDTContext(state: ModalityState, noinline block: suspend CoroutineScope.() -> T): T { suspend inline fun <T> withEDTContext(state: ModalityState, noinline block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.EDT + state.asContextElement(), block = block) return withEDTContext(state.asContextElement(), block = block)
}
suspend inline fun <T> withEDTContext(context: CoroutineContext, noinline block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.EDT + context, block = block)
} }
suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend CoroutineScope.() -> T): T { suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend CoroutineScope.() -> T): T {
@ -50,9 +56,19 @@ suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend Cor
} }
suspend inline fun <T> runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T { suspend inline fun <T> runInterruptibleEDT(state: ModalityState, noinline targetAction: () -> T): T {
return runInterruptible(Dispatchers.EDT + state.asContextElement(), block = targetAction) return runInterruptibleEDT(state.asContextElement(), targetAction = targetAction)
}
suspend inline fun <T> runInterruptibleEDT(context: CoroutineContext, noinline targetAction: () -> T): T {
return runInterruptible(Dispatchers.EDT + context, block = targetAction)
} }
fun CoroutineScope.launchWithEDT(state: ModalityState, block: suspend CoroutineScope.() -> Unit): Job { fun CoroutineScope.launchWithEDT(state: ModalityState, block: suspend CoroutineScope.() -> Unit): Job {
return launch(Dispatchers.EDT + state.asContextElement(), block = block) return launchWithEDT(state.asContextElement(), block = block)
}
fun CoroutineScope.launchWithEDT(context: CoroutineContext, block: suspend CoroutineScope.() -> Unit): Job {
return launch(Dispatchers.EDT + context, block = block)
}
fun Component.asContextElement(): CoroutineContext {
return ModalityState.stateForComponent(this).asContextElement()
} }

View file

@ -0,0 +1,78 @@
/*
* 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.shared.downloader
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.isDirectory
enum class DirectoryState {
Invalid,
NotAbsolute,
NotDirectory,
NotEmpty,
CreateNew,
Ok;
fun isValid(): Boolean {
return when(this) {
Invalid, NotAbsolute, NotDirectory, NotEmpty -> false
CreateNew, Ok -> true
}
}
companion object {
@JvmStatic
fun determine(path: Path?): DirectoryState {
if (path == null) {
return Invalid
}
if (!path.isAbsolute) {
return NotAbsolute
}
if (!path.exists()) {
var parent: Path? = path.parent
while(parent != null) {
if (!parent.exists()) {
parent = parent.parent
continue
}
if (!parent.isDirectory()) {
return NotDirectory
}
return CreateNew
}
return Invalid
}
if (!path.isDirectory()) {
return NotDirectory
}
val isEmpty = Files.newDirectoryStream(path).use { !it.iterator().hasNext() }
if (!isEmpty) {
return NotEmpty
}
return Ok
}
}
}

View file

@ -0,0 +1,172 @@
/*
* 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.shared.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT
import com.intellij.icons.AllIcons
import com.intellij.openapi.observable.util.whenFocusGained
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.textFieldWithBrowseButton
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.awt.Component
import java.nio.file.Path
import java.util.Vector
import javax.swing.DefaultComboBoxModel
import javax.swing.JList
import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString
abstract class Downloader<T, V: VersionInfo>(val component: Component) {
suspend fun download(): T? {
val info = withModalProgress(
ModalTaskOwner.component(component),
versionInfoFetchTitle,
TaskCancellation.cancellable()
) {
downloadVersionList()
}
val selector = localSelector()
val (downloadPath, version) = runInterruptibleEDT(component.asContextElement()) {
selectVersion(info, selector)
} ?: return null
withModalProgress(
ModalTaskOwner.component(component),
downloadProgressTitle(version),
TaskCancellation.cancellable()
) {
version.downloadAndUnpack(downloadPath)
}
return selector.browse(downloadPath)
}
protected abstract val windowTitle: String
protected abstract val versionInfoFetchTitle: @NlsContexts.ProgressTitle String
protected abstract fun downloadProgressTitle(version: V): @NlsContexts.ProgressTitle String
protected abstract fun localSelector(): LocalSelector<T>
protected abstract suspend fun downloadVersionList(): List<V>
protected abstract fun getSuggestedPath(): Path?
@RequiresEdt
private fun selectVersion(info: List<V>, selector: LocalSelector<T>): Pair<Path, V>? {
val dialog = DialogBuilder()
val theList = ComboBox(DefaultComboBoxModel(Vector(info)))
theList.renderer = object: ColoredListCellRenderer<V>() {
override fun customizeCellRenderer(
list: JList<out V>,
value: V?,
index: Int,
selected: Boolean,
hasFocus: Boolean
) {
value?.let { append(it.version.rawVersion) }
}
}
val outputPath = textFieldWithBrowseButton(null, selector.descriptor)
Disposer.register(dialog, outputPath)
outputPath.textField.columns = 50
lateinit var errorMessageBox: JBLabel
fun onChanged() {
val path = outputPath.text.ifBlank { null }?.toNioPathOrNull()
val state = DirectoryState.Companion.determine(path)
if (state.isValid()) {
errorMessageBox.icon = AllIcons.General.Information
dialog.setOkActionEnabled(true)
} else {
errorMessageBox.icon = AllIcons.General.Error
dialog.setOkActionEnabled(false)
}
errorMessageBox.text = ZigBrainsBundle.message(when(state) {
DirectoryState.Invalid -> "settings.shared.downloader.state.invalid"
DirectoryState.NotAbsolute -> "settings.shared.downloader.state.not-absolute"
DirectoryState.NotDirectory -> "settings.shared.downloader.state.not-directory"
DirectoryState.NotEmpty -> "settings.shared.downloader.state.not-empty"
DirectoryState.CreateNew -> "settings.shared.downloader.state.create-new"
DirectoryState.Ok -> "settings.shared.downloader.state.ok"
})
dialog.window.repaint()
}
outputPath.whenFocusGained {
onChanged()
}
outputPath.addDocumentListener(object: DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
onChanged()
}
})
var archiveSizeCell: Cell<*>? = null
fun detect(item: V) {
outputPath.text = getSuggestedPath()?.resolve(item.version.rawVersion)?.pathString ?: ""
val size = item.dist.size
val sizeMb = size / (1024f * 1024f)
archiveSizeCell?.comment?.text = ZigBrainsBundle.message("settings.shared.downloader.archive-size.text", "%.2fMB".format(sizeMb))
}
theList.addItemListener {
@Suppress("UNCHECKED_CAST")
detect(it.item as V)
}
val center = panel {
row(ZigBrainsBundle.message("settings.shared.downloader.version.label")) {
cell(theList).resizableColumn().align(AlignX.FILL)
}
row(ZigBrainsBundle.message("settings.shared.downloader.location.label")) {
cell(outputPath).resizableColumn().align(AlignX.FILL).apply { archiveSizeCell = comment("") }
}
row {
errorMessageBox = JBLabel()
cell(errorMessageBox)
}
}
detect(info[0])
dialog.centerPanel(center)
dialog.setTitle(windowTitle)
dialog.addCancelAction()
dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.shared.downloader.ok-action")) }
if (!dialog.showAndGet()) {
return null
}
val path = outputPath.text.ifBlank { null }?.toNioPathOrNull()
?: return null
if (!DirectoryState.Companion.determine(path).isValid()) {
return null
}
val version = theList.item ?: return null
return path to version
}
}

View file

@ -0,0 +1,138 @@
/*
* 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.shared.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextField
import com.intellij.ui.components.textFieldWithBrowseButton
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.panel
import com.intellij.util.concurrency.annotations.RequiresEdt
import java.awt.Component
import java.nio.file.Path
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.Icon
import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString
abstract class LocalSelector<T>(val component: Component) {
suspend open fun browse(preSelected: Path? = null): T? {
return withEDTContext(component.asContextElement()) {
doBrowseFromDisk(preSelected)
}
}
abstract val windowTitle: String
abstract val descriptor: FileChooserDescriptor
protected abstract suspend fun verify(path: Path): VerifyResult
protected abstract suspend fun resolve(path: Path, name: String?): T?
@RequiresEdt
private suspend fun doBrowseFromDisk(preSelected: Path?): T? {
val dialog = DialogBuilder()
val name = JBTextField().also { it.columns = 25 }
val path = textFieldWithBrowseButton(null, descriptor)
Disposer.register(dialog, path)
lateinit var errorMessageBox: JBLabel
suspend fun verifyAndUpdate(path: Path?) {
val result = path?.let { verify(it) } ?: VerifyResult(
"",
false,
AllIcons.General.Error,
ZigBrainsBundle.message("settings.shared.local-selector.state.invalid")
)
val prevNameDefault = name.emptyText.text.trim() == name.text.trim() || name.text.isBlank()
name.emptyText.text = result.name ?: ""
if (prevNameDefault) {
name.text = name.emptyText.text
}
errorMessageBox.icon = result.errorIcon
errorMessageBox.text = result.errorText
dialog.setOkActionEnabled(result.allowed)
}
val active = AtomicBoolean(false)
path.addDocumentListener(object: DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
if (!active.get())
return
zigCoroutineScope.launchWithEDT(ModalityState.current()) {
verifyAndUpdate(path.text.ifBlank { null }?.toNioPathOrNull())
}
}
})
val center = panel {
row(ZigBrainsBundle.message("settings.shared.local-selector.name.label")) {
cell(name).resizableColumn().align(AlignX.FILL)
}
row(ZigBrainsBundle.message("settings.shared.local-selector.path.label")) {
cell(path).resizableColumn().align(AlignX.FILL)
}
row {
errorMessageBox = JBLabel()
cell(errorMessageBox)
}
}
dialog.centerPanel(center)
dialog.setTitle(windowTitle)
dialog.addCancelAction()
dialog.addOkAction().also { it.setText(ZigBrainsBundle.message("settings.shared.local-selector.ok-action")) }
if (preSelected == null) {
val chosenFile = FileChooser.chooseFile(descriptor, null, null)
if (chosenFile != null) {
verifyAndUpdate(chosenFile.toNioPath())
path.text = chosenFile.path
}
} else {
verifyAndUpdate(preSelected)
path.text = preSelected.pathString
}
active.set(true)
if (!dialog.showAndGet()) {
active.set(false)
return null
}
active.set(false)
return path.text.ifBlank { null }?.toNioPathOrNull()?.let { resolve(it, name.text.ifBlank { null }) }
}
@JvmRecord
data class VerifyResult(
val name: String?,
val allowed: Boolean,
val errorIcon: Icon,
val errorText: String,
)
}

View file

@ -0,0 +1,164 @@
/*
* 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.shared.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.downloader.ZigVersionInfo
import com.falsepattern.zigbrains.shared.Unarchiver
import com.falsepattern.zigbrains.shared.downloader.VersionInfo.Tarball
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.util.progress.ProgressReporter
import com.intellij.platform.util.progress.reportProgress
import com.intellij.util.download.DownloadableFileService
import com.intellij.util.io.createDirectories
import com.intellij.util.io.delete
import com.intellij.util.io.move
import com.intellij.util.system.CpuArch
import com.intellij.util.system.OS
import com.intellij.util.text.SemVer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.ExperimentalPathApi
import kotlin.io.path.deleteRecursively
import kotlin.io.path.isDirectory
import kotlin.io.path.name
interface VersionInfo {
val version: SemVer
val date: String
val dist: Tarball
@Throws(Exception::class)
suspend fun downloadAndUnpack(into: Path) {
reportProgress { reporter ->
into.createDirectories()
val tarball = downloadTarball(dist, into, reporter)
unpackTarball(tarball, into, reporter)
tarball.delete()
flattenDownloadDir(into, reporter)
}
}
@JvmRecord
@Serializable
data class Tarball(val tarball: String, val shasum: String, val size: Int)
}
suspend fun downloadTarball(dist: Tarball, into: Path, reporter: ProgressReporter): Path {
return withContext(Dispatchers.IO) {
val service = DownloadableFileService.getInstance()
val fileName = dist.tarball.substringAfterLast('/')
val tempFile = FileUtil.createTempFile(into.toFile(), "tarball", fileName, false, false)
val desc = service.createFileDescription(dist.tarball, tempFile.name)
val downloader = service.createDownloader(listOf(desc), ZigBrainsBundle.message("settings.toolchain.downloader.service.tarball"))
val downloadResults = reporter.sizedStep(100) {
coroutineToIndicator {
downloader.download(into.toFile())
}
}
if (downloadResults.isEmpty())
throw IllegalStateException("No file downloaded")
return@withContext downloadResults[0].first.toPath()
}
}
suspend fun flattenDownloadDir(dir: Path, reporter: ProgressReporter) {
withContext(Dispatchers.IO) {
val contents = Files.newDirectoryStream(dir).use { it.toList() }
if (contents.size == 1 && contents[0].isDirectory()) {
val src = contents[0]
reporter.indeterminateStep {
coroutineToIndicator {
val indicator = ProgressManager.getInstance().progressIndicator ?: EmptyProgressIndicator()
indicator.isIndeterminate = true
indicator.text = ZigBrainsBundle.message("settings.toolchain.downloader.progress.flatten")
Files.newDirectoryStream(src).use { stream ->
stream.forEach {
indicator.text2 = it.name
it.move(dir.resolve(src.relativize(it)))
}
}
}
}
src.delete()
}
}
}
@OptIn(ExperimentalPathApi::class)
suspend fun unpackTarball(tarball: Path, into: Path, reporter: ProgressReporter) {
withContext(Dispatchers.IO) {
try {
reporter.indeterminateStep {
coroutineToIndicator {
Unarchiver.unarchive(tarball, into)
}
}
} catch (e: Throwable) {
tarball.delete()
val contents = Files.newDirectoryStream(into).use { it.toList() }
if (contents.size == 1 && contents[0].isDirectory()) {
contents[0].deleteRecursively()
}
throw e
}
}
}
fun getTarballIfCompatible(dist: String, tb: JsonElement): Tarball? {
if (!dist.contains('-'))
return null
val (arch, os) = dist.split('-', limit = 2)
val theArch = when (arch) {
"x86_64" -> CpuArch.X86_64
"i386", "x86" -> CpuArch.X86
"armv7a" -> CpuArch.ARM32
"aarch64" -> CpuArch.ARM64
else -> return null
}
val theOS = when (os) {
"linux" -> OS.Linux
"windows" -> OS.Windows
"macos" -> OS.macOS
"freebsd" -> OS.FreeBSD
else -> return null
}
if (theArch != CpuArch.CURRENT || theOS != OS.CURRENT) {
return null
}
return Json.decodeFromJsonElement<Tarball>(tb)
}
val tempPluginDir get(): File = PathManager.getTempPath().toNioPathOrNull()!!.resolve("zigbrains").toFile()

View file

@ -22,7 +22,7 @@
package com.falsepattern.zigbrains.shared.ipc package com.falsepattern.zigbrains.shared.ipc
import com.falsepattern.zigbrains.direnv.emptyEnv import com.falsepattern.zigbrains.direnv.Env
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.util.SystemInfo import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil import com.intellij.openapi.util.io.FileUtil
@ -56,7 +56,7 @@ object IPCUtil {
if (SystemInfo.isWindows) { if (SystemInfo.isWindows) {
return null return null
} }
val mkfifo = emptyEnv val mkfifo = Env.empty
.findAllExecutablesOnPATH("mkfifo") .findAllExecutablesOnPATH("mkfifo")
.map { it.pathString } .map { it.pathString }
.map(::MKFifo) .map(::MKFifo)
@ -67,7 +67,7 @@ object IPCUtil {
true true
} ?: return null } ?: return null
val selectedBash = emptyEnv val selectedBash = Env.empty
.findAllExecutablesOnPATH("bash") .findAllExecutablesOnPATH("bash")
.map { it.pathString } .map { it.pathString }
.filter { .filter {

View file

@ -0,0 +1,37 @@
/*
* 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.shared.ui
import com.falsepattern.zigbrains.shared.UUIDMapSerializable
import com.intellij.openapi.ui.NamedConfigurable
import java.awt.Component
import java.util.UUID
interface UUIDComboBoxDriver<T> {
val theMap: UUIDMapSerializable.Converting<T, *, *>
suspend fun constructModelList(): List<ListElemIn<T>>
fun createContext(model: ZBModel<T>): ZBContext<T>
fun createComboBox(model: ZBModel<T>): ZBComboBox<T>
suspend fun resolvePseudo(context: Component, elem: ListElem.Pseudo<T>): UUID?
fun createNamedConfigurable(uuid: UUID, elem: T): NamedConfigurable<UUID>
}

View file

@ -0,0 +1,169 @@
/*
* 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.shared.ui
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.StorageChangeListener
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.observable.util.whenListChanged
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.MasterDetailsComponent
import com.intellij.util.IconUtil
import com.intellij.util.asSafely
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.launch
import java.util.UUID
import javax.swing.JComponent
import javax.swing.tree.DefaultTreeModel
abstract class UUIDMapEditor<T>(val driver: UUIDComboBoxDriver<T>): MasterDetailsComponent() {
private var isTreeInitialized = false
private var registered: Boolean = false
private var selectOnNextReload: UUID? = null
private var disposed: Boolean = false
private val changeListener: StorageChangeListener = { this@UUIDMapEditor.listChanged() }
override fun createComponent(): JComponent {
if (!isTreeInitialized) {
initTree()
isTreeInitialized = true
}
if (!registered) {
driver.theMap.addChangeListener(changeListener)
registered = true
}
return super.createComponent()
}
override fun createActions(fromPopup: Boolean): List<AnAction> {
val add = object : DumbAwareAction({ ZigBrainsBundle.message("settings.shared.list.add-action.name") }, Presentation.NULL_STRING, IconUtil.addIcon) {
override fun actionPerformed(e: AnActionEvent) {
zigCoroutineScope.launchWithEDT(ModalityState.current()) {
if (disposed)
return@launchWithEDT
val modelList = driver.constructModelList()
val model = ZBModel(modelList)
val context = driver.createContext(model)
val popup = ZBComboBoxPopup(context, null, ::onItemSelected)
model.whenListChanged {
popup.syncWithModelChange()
}
popup.showInBestPositionFor(e.dataContext)
}
}
}
return listOf(add, MyDeleteAction())
}
override fun onItemDeleted(item: Any?) {
if (item is UUID) {
driver.theMap.remove(item)
}
super.onItemDeleted(item)
}
private fun onItemSelected(elem: ListElem<T>) {
if (elem !is ListElem.Pseudo)
return
zigCoroutineScope.launch(myWholePanel.asContextElement()) {
if (disposed)
return@launch
val uuid = driver.resolvePseudo(myWholePanel, elem)
if (uuid != null) {
withEDTContext(myWholePanel.asContextElement()) {
applyUUIDNowOrOnReload(uuid)
}
}
}
}
override fun reset() {
reloadTree()
super.reset()
}
override fun getEmptySelectionString() = ZigBrainsBundle.message("settings.shared.list.empty")
override fun disposeUIResources() {
disposed = true
super.disposeUIResources()
if (registered) {
driver.theMap.removeChangeListener(changeListener)
}
}
private fun addElem(uuid: UUID, elem: T) {
val node = MyNode(driver.createNamedConfigurable(uuid, elem))
addNode(node, myRoot)
}
private fun reloadTree() {
if (disposed)
return
val currentSelection = selectedObject?.asSafely<UUID>()
selectedNode = null
myRoot.removeAllChildren()
(myTree.model as DefaultTreeModel).reload()
val onReload = selectOnNextReload
selectOnNextReload = null
var hasOnReload = false
driver.theMap.forEach { (uuid, elem) ->
addElem(uuid, elem)
if (uuid == onReload) {
hasOnReload = true
}
}
(myTree.model as DefaultTreeModel).reload()
if (hasOnReload) {
selectedNode = findNodeByObject(myRoot, onReload)
return
}
selectedNode = currentSelection?.let { findNodeByObject(myRoot, it) }
}
@RequiresEdt
private fun applyUUIDNowOrOnReload(uuid: UUID?) {
selectNodeInTree(uuid)
val currentSelection = selectedObject?.asSafely<UUID>()
if (uuid != null && uuid != currentSelection) {
selectOnNextReload = uuid
} else {
selectOnNextReload = null
}
}
private suspend fun listChanged() {
if (disposed)
return
withEDTContext(myWholePanel.asContextElement()) {
reloadTree()
}
}
}

View file

@ -0,0 +1,193 @@
/*
* 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.shared.ui
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.StorageChangeListener
import com.falsepattern.zigbrains.shared.coroutine.asContextElement
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.ui.ListElem
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.observable.util.whenListChanged
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Row
import com.intellij.util.concurrency.annotations.RequiresEdt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.awt.event.ItemEvent
import java.util.UUID
import javax.swing.JButton
abstract class UUIDMapSelector<T>(val driver: UUIDComboBoxDriver<T>): Disposable {
private val comboBox: ZBComboBox<T>
private var selectOnNextReload: UUID? = null
private val model: ZBModel<T>
private var editButton: JButton? = null
private val changeListener: StorageChangeListener = { this@UUIDMapSelector.listChanged() }
init {
model = ZBModel(emptyList())
comboBox = driver.createComboBox(model)
comboBox.addItemListener(::itemStateChanged)
driver.theMap.addChangeListener(changeListener)
model.whenListChanged {
zigCoroutineScope.launchWithEDT(comboBox.asContextElement()) {
tryReloadSelection()
}
if (comboBox.isPopupVisible) {
comboBox.isPopupVisible = false
comboBox.isPopupVisible = true
}
}
zigCoroutineScope.launchWithEDT(ModalityState.any()) {
model.updateContents(driver.constructModelList())
}
}
protected var selectedUUID: UUID?
get() = comboBox.selectedUUID
set(value) {
zigCoroutineScope.launchWithEDT(ModalityState.any()) {
applyUUIDNowOrOnReload(value)
}
}
protected open fun onSelection(uuid: UUID?) {}
private fun refreshButtonState(item: ListElem<*>) {
val actual = item is ListElem.One.Actual<*>
editButton?.isEnabled = actual
editButton?.repaint()
onSelection(if (actual) item.uuid else null)
}
private fun itemStateChanged(event: ItemEvent) {
if (event.stateChange != ItemEvent.SELECTED) {
return
}
val item = event.item
if (item !is ListElem<*>)
return
refreshButtonState(item)
if (item !is ListElem.Pseudo<*>)
return
@Suppress("UNCHECKED_CAST")
item as ListElem.Pseudo<T>
zigCoroutineScope.launch(comboBox.asContextElement()) {
val uuid = runCatching { driver.resolvePseudo(comboBox, item) }.getOrNull()
delay(100)
withEDTContext(comboBox.asContextElement()) {
applyUUIDNowOrOnReload(uuid)
}
}
}
@RequiresEdt
private fun tryReloadSelection() {
val list = model.toList()
if (list.size == 1) {
comboBox.selectedItem = list[0]
comboBox.isEnabled = false
return
}
comboBox.isEnabled = true
val onReload = selectOnNextReload
selectOnNextReload = null
if (onReload != null) {
val element = list.firstOrNull { when(it) {
is ListElem.One.Actual<*> -> it.uuid == onReload
else -> false
} }
if (element == null) {
selectOnNextReload = onReload
} else {
comboBox.selectedItem = element
return
}
}
val selected = model.selected
if (selected != null && list.contains(selected)) {
comboBox.selectedItem = selected
return
}
if (selected is ListElem.One.Actual<*>) {
val uuid = selected.uuid
val element = list.firstOrNull { when(it) {
is ListElem.One.Actual -> it.uuid == uuid
else -> false
} }
comboBox.selectedItem = element
return
}
comboBox.selectedItem = ListElem.None<Any>()
}
protected suspend fun listChanged() {
withContext(Dispatchers.EDT + comboBox.asContextElement()) {
val list = driver.constructModelList()
model.updateContents(list)
tryReloadSelection()
}
}
protected fun attachComboBoxRow(row: Row): Unit = with(row) {
cell(comboBox).resizableColumn().align(AlignX.FILL)
button(ZigBrainsBundle.message("settings.toolchain.editor.toolchain.edit-button.name")) { e ->
zigCoroutineScope.launchWithEDT(comboBox.asContextElement()) {
var selectedUUID = comboBox.selectedUUID ?: return@launchWithEDT
val elem = driver.theMap[selectedUUID] ?: return@launchWithEDT
val config = driver.createNamedConfigurable(selectedUUID, elem)
val apply = ShowSettingsUtil.getInstance().editConfigurable(DialogWrapper.findInstance(comboBox)?.contentPane, config)
if (apply) {
applyUUIDNowOrOnReload(selectedUUID)
}
}
}.component.let {
editButton = it
}
}
@RequiresEdt
private fun applyUUIDNowOrOnReload(uuid: UUID?) {
comboBox.selectedUUID = uuid
if (uuid != null && comboBox.selectedUUID == null) {
selectOnNextReload = uuid
} else {
selectOnNextReload = null
}
}
override fun dispose() {
zigToolchainList.removeChangeListener(changeListener)
}
}

View file

@ -0,0 +1,86 @@
/*
* 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.shared.ui
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.UUID
sealed interface ListElemIn<T>
@Suppress("UNCHECKED_CAST")
sealed interface ListElem<T> : ListElemIn<T> {
sealed interface Pseudo<T>: ListElem<T>
sealed interface One<T> : ListElem<T> {
val instance: T
@JvmRecord
data class Suggested<T>(override val instance: T): One<T>, Pseudo<T>
@JvmRecord
data class Actual<T>(val uuid: UUID, override val instance: T): One<T>
}
class None<T> private constructor(): ListElem<T> {
companion object {
private val INSTANCE = None<Any>()
operator fun <T> invoke(): None<T> {
return INSTANCE as None<T>
}
}
}
class Download<T> private constructor(): ListElem<T>, Pseudo<T> {
companion object {
private val INSTANCE = Download<Any>()
operator fun <T> invoke(): Download<T> {
return INSTANCE as Download<T>
}
}
}
class FromDisk<T> private constructor(): ListElem<T>, Pseudo<T> {
companion object {
private val INSTANCE = FromDisk<Any>()
operator fun <T> invoke(): FromDisk<T> {
return INSTANCE as FromDisk<T>
}
}
}
data class Pending<T>(val elems: Flow<ListElem<T>>): ListElem<T>
companion object {
private val fetchGroup: List<ListElem<Any>> = listOf(Download(), FromDisk())
fun <T> fetchGroup() = fetchGroup as List<ListElem<T>>
}
}
@JvmRecord
data class Separator<T>(val text: String, val line: Boolean) : ListElemIn<T>
fun <T> Pair<UUID, T>.asActual() = ListElem.One.Actual(first, second)
fun <T> T.asSuggested() = ListElem.One.Suggested(this)
@JvmName("listElemFlowAsPending")
fun <T> Flow<ListElem<T>>.asPending() = ListElem.Pending(this)
fun <T> Flow<T>.asPending() = map { it.asSuggested() }.asPending()

View file

@ -0,0 +1,295 @@
/*
* 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.shared.ui
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.text.StringUtil
import com.intellij.ui.CellRendererPanel
import com.intellij.ui.CollectionComboBoxModel
import com.intellij.ui.ColoredListCellRenderer
import com.intellij.ui.GroupHeaderSeparator
import com.intellij.ui.SimpleColoredComponent
import com.intellij.ui.SimpleTextAttributes
import com.intellij.ui.components.panels.OpaquePanel
import com.intellij.ui.popup.list.ComboBoxPopup
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 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.function.Consumer
import javax.accessibility.AccessibleContext
import javax.swing.JList
import javax.swing.border.Border
import kotlin.io.path.pathString
class ZBComboBoxPopup<T>(
context: ZBContext<T>,
selected: ListElem<T>?,
onItemSelected: Consumer<ListElem<T>>,
) : ComboBoxPopup<ListElem<T>>(context, selected, onItemSelected)
open class ZBComboBox<T>(model: ZBModel<T>, renderer: (() -> ZBModel<T>)-> ZBCellRenderer<T>): ComboBox<ListElem<T>>(model) {
init {
setRenderer(renderer { model })
}
var selectedUUID: UUID?
set(value) {
if (value == null) {
selectedItem = ListElem.None<Any>()
return
}
for (i in 0..<model.size) {
val element = model.getElementAt(i)
if (element is ListElem.One.Actual) {
if (element.uuid == value) {
selectedIndex = i
return
}
}
}
selectedItem = ListElem.None<Any>()
}
get() {
val item = selectedItem
return when(item) {
is ListElem.One.Actual<*> -> item.uuid
else -> null
}
}
}
class ZBModel<T> private constructor(elements: List<ListElem<T>>, private var separators: MutableMap<ListElem<T>, Separator<T>>) : CollectionComboBoxModel<ListElem<T>>(elements) {
private var counter: Int = 0
companion object {
operator fun <T> invoke(input: List<ListElemIn<T>>): ZBModel<T> {
val (elements, separators) = convert(input)
val model = ZBModel<T>(elements, separators)
model.launchPendingResolve()
return model
}
private fun <T> convert(input: List<ListElemIn<T>>): Pair<List<ListElem<T>>, MutableMap<ListElem<T>, Separator<T>>> {
val separators = IdentityHashMap<ListElem<T>, Separator<T>>()
var lastSeparator: Separator<T>? = null
val elements = ArrayList<ListElem<T>>()
input.forEach {
when (it) {
is ListElem -> {
if (lastSeparator != null) {
separators[it] = lastSeparator
lastSeparator = null
}
elements.add(it)
}
is Separator -> lastSeparator = it
}
}
return elements to separators
}
}
fun separatorAbove(elem: ListElem<T>) = separators[elem]
private fun launchPendingResolve() {
runInEdt(ModalityState.any()) {
val counter = this.counter
val size = this.size
for (i in 0..<size) {
val elem = getElementAt(i)
?: continue
if (elem !is ListElem.Pending)
continue
zigCoroutineScope.launch(Dispatchers.EDT + ModalityState.any().asContextElement()) {
elem.elems.collect { newElem ->
insertBefore(elem, newElem, counter)
}
remove(elem, counter)
}
}
}
}
@RequiresEdt
private fun remove(old: ListElem<T>, oldCounter: Int) {
val newCounter = this@ZBModel.counter
if (oldCounter != newCounter) {
return
}
val index = this@ZBModel.getElementIndex(old)
this@ZBModel.remove(index)
val sep = separators.remove(old)
if (sep != null && this@ZBModel.size > index) {
this@ZBModel.getElementAt(index)?.let { separators[it] = sep }
}
}
@RequiresEdt
private fun insertBefore(old: ListElem<T>, new: ListElem<T>?, oldCounter: Int) {
val newCounter = this@ZBModel.counter
if (oldCounter != newCounter) {
return
}
if (new == null) {
return
}
val currentIndex = this@ZBModel.getElementIndex(old)
separators.remove(old)?.let {
separators.put(new, it)
}
this@ZBModel.add(currentIndex, new)
}
@RequiresEdt
fun updateContents(input: List<ListElemIn<T>>) {
counter++
val (elements, separators) = convert(input)
this.separators = separators
replaceAll(elements)
launchPendingResolve()
}
}
open class ZBContext<T>(private val project: Project?, private val model: ZBModel<T>, private val getRenderer: (() -> ZBModel<T>) -> ZBCellRenderer<T>) : ComboBoxPopup.Context<ListElem<T>> {
override fun getProject(): Project? {
return project
}
override fun getModel(): ZBModel<T> {
return model
}
override fun getRenderer(): ZBCellRenderer<T> {
return getRenderer(::getModel)
}
}
abstract class ZBCellRenderer<T>(val getModel: () -> ZBModel<T>) : ColoredListCellRenderer<ListElem<T>>() {
final override fun getListCellRendererComponent(
list: JList<out ListElem<T>?>?,
value: ListElem<T>?,
index: Int,
selected: Boolean,
hasFocus: Boolean
): Component? {
val component = super.getListCellRendererComponent(list, value, index, selected, hasFocus) as SimpleColoredComponent
val panel = object : CellRendererPanel(BorderLayout()) {
val myContext = component.accessibleContext
override fun getAccessibleContext(): AccessibleContext? {
return myContext
}
override fun setBorder(border: Border?) {
component.border = border
}
}
panel.add(component, BorderLayout.CENTER)
component.isOpaque = true
list?.let { background = if (selected) it.selectionBackground else it.background }
val model = getModel()
if (index == -1) {
component.isOpaque = false
panel.isOpaque = false
return panel
}
val separator = value?.let { model.separatorAbove(it) }
if (separator != null) {
val vGap = if (UIUtil.isUnderNativeMacLookAndFeel()) 1 else 3
val separatorComponent = GroupHeaderSeparator(JBUI.insets(vGap, 10, vGap, 0))
separatorComponent.isHideLine = !separator.line
separatorComponent.caption = separator.text.ifBlank { null }
val wrapper = OpaquePanel(BorderLayout())
wrapper.add(separatorComponent, BorderLayout.CENTER)
list?.let { wrapper.background = it.background }
panel.add(wrapper, BorderLayout.NORTH)
}
return panel
}
abstract override fun customizeCellRenderer(
list: JList<out ListElem<T>?>,
value: ListElem<T>?,
index: Int,
selected: Boolean,
hasFocus: Boolean
)
}
fun renderPathNameComponent(path: String, name: String?, nameFallback: String, component: SimpleColoredComponent, isSuggestion: Boolean, isSelected: Boolean) {
val path = presentDetectedPath(path)
val primary: String
var secondary: String?
val tooltip: String?
if (isSuggestion) {
primary = path
secondary = name
} else {
primary = name ?: nameFallback
secondary = path
}
if (isSelected) {
tooltip = secondary
secondary = null
} else {
tooltip = null
}
component.append(primary)
if (secondary != null) {
component.append(" ")
component.append(secondary, SimpleTextAttributes.GRAYED_ATTRIBUTES)
}
component.toolTipText = tooltip
}
fun presentDetectedPath(home: String, maxLength: Int = 50, suffixLength: Int = 30): String {
//for macOS, let's try removing Bundle internals
var home = home
home = StringUtil.trimEnd(home, "/Contents/Home") //NON-NLS
home = StringUtil.trimEnd(home, "/Contents/MacOS") //NON-NLS
home = FileUtil.getLocationRelativeToUserHome(home, false)
home = StringUtil.shortenTextWithEllipsis(home, maxLength, suffixLength)
return home
}
private val EMPTY_ICON = EmptyIcon.create(1, 16)

View file

@ -142,7 +142,15 @@
parentId="language" parentId="language"
instance="com.falsepattern.zigbrains.project.settings.ZigConfigurable" instance="com.falsepattern.zigbrains.project.settings.ZigConfigurable"
id="ZigConfigurable" id="ZigConfigurable"
displayName="Zig" bundle="zigbrains.Bundle"
key="settings.project.display-name"
/>
<applicationConfigurable
parentId="ZigConfigurable"
instance="com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainListEditor"
id="ZigToolchainConfigurable"
bundle="zigbrains.Bundle"
key="settings.toolchain.list.title"
/> />
<programRunner <programRunner
@ -158,7 +166,7 @@
/> />
<additionalLibraryRootsProvider <additionalLibraryRootsProvider
implementation="com.falsepattern.zigbrains.project.toolchain.stdlib.ZigLibraryRootProvider" implementation="com.falsepattern.zigbrains.project.stdlib.ZigLibraryRootProvider"
/> />
<!--suppress PluginXmlValidity --> <!--suppress PluginXmlValidity -->
@ -177,10 +185,13 @@
<extensions defaultExtensionNs="com.falsepattern.zigbrains"> <extensions defaultExtensionNs="com.falsepattern.zigbrains">
<toolchainProvider <toolchainProvider
implementation="com.falsepattern.zigbrains.project.toolchain.LocalZigToolchainProvider" implementation="com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchainProvider"
/> />
<projectConfigProvider <projectConfigProvider
implementation="com.falsepattern.zigbrains.project.settings.ZigCoreProjectConfigurationProvider" implementation="com.falsepattern.zigbrains.project.toolchain.ui.ZigToolchainEditor$Provider"
/>
<projectConfigProvider
implementation="com.falsepattern.zigbrains.direnv.ui.DirenvEditor$Provider"
/> />
</extensions> </extensions>

View file

@ -110,3 +110,47 @@ build.tool.window.status.error.general=Error while running zig build -l
build.tool.window.status.no-builds=No builds currently in progress build.tool.window.status.no-builds=No builds currently in progress
build.tool.window.status.timeout=zig build -l timed out after {0} seconds. build.tool.window.status.timeout=zig build -l timed out after {0} seconds.
zig=Zig zig=Zig
settings.shared.list.add-action.name=Add New
settings.shared.list.empty=Select an entry to view or edit its details here
settings.shared.downloader.version.label=Version:
settings.shared.downloader.location.label=Location:
settings.shared.downloader.ok-action=Download
settings.shared.downloader.state.invalid=Invalid path
settings.shared.downloader.state.not-absolute=Must be an absolute path
settings.shared.downloader.state.not-directory=Path is not a directory
settings.shared.downloader.state.not-empty=Directory is not empty
settings.shared.downloader.state.create-new=Directory will be created
settings.shared.downloader.state.ok=Directory OK
settings.shared.downloader.archive-size.text=Archive size: {0}
settings.shared.local-selector.name.label=Name:
settings.shared.local-selector.path.label=Path:
settings.shared.local-selector.ok-action=Add
settings.shared.local-selector.state.invalid=Invalid path
settings.project.display-name=Zig
settings.toolchain.base.name.label=Name
settings.toolchain.local.path.label=Toolchain location
settings.toolchain.local.version.label=Detected zig version
settings.toolchain.local.std.label=Override standard library
settings.toolchain.editor.toolchain.label=Toolchain
settings.toolchain.editor.toolchain-default.label=Default toolchain
settings.toolchain.editor.toolchain.edit-button.name=Edit
settings.toolchain.model.detected.separator=Detected toolchains
settings.toolchain.model.none.text=<No Toolchain>
settings.toolchain.model.loading.text=Loading\u2026
settings.toolchain.model.from-disk.text=Add Zig from disk\u2026
settings.toolchain.model.download.text=Download Zig\u2026
settings.toolchain.list.title=Toolchains
settings.toolchain.downloader.title=Install Zig
settings.toolchain.downloader.progress.fetch=Fetching zig version information
settings.toolchain.downloader.progress.install=Installing Zig {0}
settings.toolchain.downloader.progress.flatten=Flattening unpacked archive
settings.toolchain.downloader.chooser.title=Zig Install Directory
settings.toolchain.downloader.service.index=Zig version information
settings.toolchain.downloader.service.tarball=Zig archive
settings.toolchain.local-selector.title=Select Zig From Disk
settings.toolchain.local-selector.chooser.title=Zig Installation Directory
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

21
licenses/ZLS.LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) ZLS contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,32 @@
/*
* 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.lsp
import com.intellij.openapi.util.IconLoader
import org.jetbrains.annotations.NonNls
@NonNls
object LSPIcons {
@JvmField
val ZLS = IconLoader.getIcon("/icons/zls.svg", LSPIcons::class.java)
}

View file

@ -20,24 +20,22 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.toolchain package com.falsepattern.zigbrains.lsp
import com.falsepattern.zigbrains.lsp.config.SuspendingZLSConfigProvider import com.falsepattern.zigbrains.lsp.config.SuspendingZLSConfigProvider
import com.falsepattern.zigbrains.lsp.config.ZLSConfig import com.falsepattern.zigbrains.lsp.config.ZLSConfig
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
import kotlin.io.path.pathString import kotlin.io.path.pathString
class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider { class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider {
override suspend fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { override suspend fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig {
val svc = project.zigProjectSettings val svc = ZigToolchainService.getInstance(project)
var state = svc.state val toolchain = svc.toolchain ?: return previous
val toolchain = state.toolchain ?: ZigToolchainProvider.suggestToolchain(project, UserDataHolderBase()) ?: return previous
val env = toolchain.zig.getEnv(project).getOrElse { throwable -> val env = toolchain.zig.getEnv(project).getOrElse { throwable ->
throwable.printStackTrace() throwable.printStackTrace()
@ -65,16 +63,10 @@ class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider {
).notify(project) ).notify(project)
return previous return previous
} }
var lib = if (state.overrideStdPath && state.explicitPathToStd != null) { var lib = if (toolchain is LocalZigToolchain)
state.explicitPathToStd?.toNioPathOrNull() ?: run { toolchain.std
Notification( else
"zigbrains-lsp",
"Invalid zig standard library path override: ${state.explicitPathToStd}",
NotificationType.ERROR
).notify(project)
null null
}
} else null
if (lib == null) { if (lib == null) {
lib = env.libDirectory.toNioPathOrNull() ?: run { lib = env.libDirectory.toNioPathOrNull() ?: run {

View file

@ -22,36 +22,26 @@
package com.falsepattern.zigbrains.lsp package com.falsepattern.zigbrains.lsp
import com.falsepattern.zigbrains.direnv.DirenvCmd import com.falsepattern.zigbrains.lsp.zls.zls
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
import com.falsepattern.zigbrains.lsp.settings.zlsSettings
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.startup.ProjectActivity
import com.intellij.ui.EditorNotifications import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.io.path.pathString
class ZLSStartup: ProjectActivity { class ZLSStartup: ProjectActivity {
override suspend fun execute(project: Project) { override suspend fun execute(project: Project) {
val zlsState = project.zlsSettings.state
if (zlsState.zlsPath.isBlank()) {
val env = if (DirenvCmd.direnvInstalled() && !project.isDefault && project.zigProjectSettings.state.direnv)
project.getDirenv()
else
emptyEnv
env.findExecutableOnPATH("zls")?.let {
zlsState.zlsPath = it.pathString
project.zlsSettings.state = zlsState
}
}
project.zigCoroutineScope.launch { project.zigCoroutineScope.launch {
var currentState = project.zlsRunningAsync() var currentState = project.zlsRunning()
var currentZLS = project.zls
while (!project.isDisposed) { while (!project.isDisposed) {
val running = project.zlsRunningAsync() val zls = project.zls
if (currentZLS != zls) {
startLSP(project, true)
}
currentZLS = zls
val running = project.zlsRunning()
if (currentState != running) { if (currentState != running) {
EditorNotifications.getInstance(project).updateAllNotifications() EditorNotifications.getInstance(project).updateAllNotifications()
} }

View file

@ -22,11 +22,9 @@
package com.falsepattern.zigbrains.lsp package com.falsepattern.zigbrains.lsp
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
import com.falsepattern.zigbrains.lsp.config.ZLSConfigProviderBase import com.falsepattern.zigbrains.lsp.config.ZLSConfigProviderBase
import com.falsepattern.zigbrains.lsp.settings.zlsSettings import com.falsepattern.zigbrains.lsp.zls.zls
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.notification.Notification import com.intellij.notification.Notification
import com.intellij.notification.NotificationType import com.intellij.notification.NotificationType
@ -55,30 +53,8 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
suspend fun getCommand(project: Project): List<String>? { suspend fun getCommand(project: Project): List<String>? {
val svc = project.zlsSettings val zls = project.zls ?: return null
val state = svc.state val zlsPath: Path = zls.path
val zlsPath: Path = state.zlsPath.let { zlsPath ->
if (zlsPath.isEmpty()) {
val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv
env.findExecutableOnPATH("zls") ?: run {
Notification(
"zigbrains-lsp",
ZLSBundle.message("notification.message.could-not-detect.content"),
NotificationType.ERROR
).notify(project)
return null
}
} else {
zlsPath.toNioPathOrNull() ?: run {
Notification(
"zigbrains-lsp",
ZLSBundle.message("notification.message.zls-exe-path-invalid.content", zlsPath),
NotificationType.ERROR
).notify(project)
return null
}
}
}
if (!zlsPath.toFile().exists()) { if (!zlsPath.toFile().exists()) {
Notification( Notification(
"zigbrains-lsp", "zigbrains-lsp",
@ -95,7 +71,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje
).notify(project) ).notify(project)
return null return null
} }
val configPath: Path? = state.zlsConfigPath.let { configPath -> val configPath: Path? = "".let { configPath ->
if (configPath.isNotBlank()) { if (configPath.isNotBlank()) {
configPath.toNioPathOrNull()?.let { nioPath -> configPath.toNioPathOrNull()?.let { nioPath ->
if (!nioPath.toFile().exists()) { if (!nioPath.toFile().exists()) {

View file

@ -22,7 +22,7 @@
package com.falsepattern.zigbrains.lsp package com.falsepattern.zigbrains.lsp
import com.falsepattern.zigbrains.lsp.settings.zlsSettings import com.falsepattern.zigbrains.lsp.zls.zls
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.components.service import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
@ -68,39 +68,29 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS
} }
features.inlayHintFeature = object: LSPInlayHintFeature() { features.inlayHintFeature = object: LSPInlayHintFeature() {
override fun isEnabled(file: PsiFile): Boolean { override fun isEnabled(file: PsiFile): Boolean {
return features.project.zlsSettings.state.inlayHints return project.zls?.settings?.inlayHints == true
} }
} }
return features return features
} }
override fun isEnabled(project: Project) = project.zlsEnabledSync() override fun isEnabled(project: Project) = project.zlsEnabled()
override fun setEnabled(enabled: Boolean, project: Project) { override fun setEnabled(enabled: Boolean, project: Project) {
project.zlsEnabled(enabled) project.zlsEnabled(enabled)
} }
} }
suspend fun Project.zlsEnabledAsync(): Boolean { fun Project.zlsEnabled(): Boolean {
return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateAsync() return (getUserData(ENABLED_KEY) != false) && zls?.isValid() == true
}
fun Project.zlsEnabledSync(): Boolean {
return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateSync()
} }
fun Project.zlsEnabled(value: Boolean) { fun Project.zlsEnabled(value: Boolean) {
putUserData(ENABLED_KEY, value) putUserData(ENABLED_KEY, value)
} }
suspend fun Project.zlsRunningAsync(): Boolean { fun Project.zlsRunning(): Boolean {
if (!zlsEnabledAsync()) if (!zlsEnabled())
return false
return lsm.isRunning
}
fun Project.zlsRunningSync(): Boolean {
if (!zlsEnabledSync())
return false return false
return lsm.isRunning return lsm.isRunning
} }
@ -135,7 +125,7 @@ private suspend fun doStart(project: Project, restart: Boolean) {
project.lsm.stop("ZigBrains") project.lsm.stop("ZigBrains")
delay(250) delay(250)
} }
if (project.zlsSettings.validateAsync()) { if (project.zls?.isValid() == true) {
delay(250) delay(250)
project.lsm.start("ZigBrains") project.lsm.start("ZigBrains")
} }

View file

@ -23,8 +23,8 @@
package com.falsepattern.zigbrains.lsp.notification package com.falsepattern.zigbrains.lsp.notification
import com.falsepattern.zigbrains.lsp.ZLSBundle import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.settings.zlsSettings import com.falsepattern.zigbrains.lsp.zls.zls
import com.falsepattern.zigbrains.lsp.zlsRunningAsync import com.falsepattern.zigbrains.lsp.zlsRunning
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.falsepattern.zigbrains.zig.ZigFileType import com.falsepattern.zigbrains.zig.ZigFileType
import com.falsepattern.zigbrains.zon.ZonFileType import com.falsepattern.zigbrains.zon.ZonFileType
@ -49,10 +49,10 @@ class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware {
else -> return null else -> return null
} }
val task = project.zigCoroutineScope.async { val task = project.zigCoroutineScope.async {
if (project.zlsRunningAsync()) { if (project.zlsRunning()) {
return@async null return@async null
} else { } else {
return@async project.zlsSettings.validateAsync() return@async project.zls?.isValid() == true
} }
} }
return Function { editor -> return Function { editor ->

View file

@ -1,158 +0,0 @@
/*
* 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.lsp.settings
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.startLSP
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.intellij.ide.IdeEventQueue
import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.util.application
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.nio.file.Path
import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile
@Service(Service.Level.PROJECT)
@State(
name = "ZLSSettings",
storages = [Storage(value = "zigbrains.xml")]
)
class ZLSProjectSettingsService(val project: Project): PersistentStateComponent<ZLSSettings> {
@Volatile
private var state = ZLSSettings()
@Volatile
private var dirty = true
@Volatile
private var valid = false
private val mutex = Mutex()
override fun getState(): ZLSSettings {
return state.copy()
}
fun setState(value: ZLSSettings) {
runBlocking {
mutex.withLock {
this@ZLSProjectSettingsService.state = value
dirty = true
}
}
startLSP(project, true)
}
override fun loadState(state: ZLSSettings) {
setState(state)
}
suspend fun validateAsync(): Boolean {
mutex.withLock {
if (dirty) {
val state = this.state
valid = doValidate(project, state)
dirty = false
}
return valid
}
}
fun validateSync(): Boolean {
val isValid: Boolean? = runBlocking {
mutex.withLock {
if (dirty)
null
else
valid
}
}
if (isValid != null) {
return isValid
}
return if (useModalProgress()) {
runWithModalProgressBlocking(ModalTaskOwner.project(project), ZLSBundle.message("progress.title.validate")) {
validateAsync()
}
} else {
runBlocking {
validateAsync()
}
}
}
}
private val prohibitClass: Class<*>? = runCatching {
Class.forName("com_intellij_ide_ProhibitAWTEvents".replace('_', '.'))
}.getOrNull()
private val postProcessors: List<*>? = runCatching {
if (prohibitClass == null)
return@runCatching null
val postProcessorsField = IdeEventQueue::class.java.getDeclaredField("postProcessors")
postProcessorsField.isAccessible = true
postProcessorsField.get(IdeEventQueue.getInstance()) as? List<*>
}.getOrNull()
private fun useModalProgress(): Boolean {
if (!application.isDispatchThread)
return false
if (application.isWriteAccessAllowed)
return false
if (postProcessors == null)
return true
return postProcessors.none { prohibitClass!!.isInstance(it) }
}
private suspend fun doValidate(project: Project, state: ZLSSettings): Boolean {
val zlsPath: Path = state.zlsPath.let { zlsPath ->
if (zlsPath.isEmpty()) {
val env = if (project.zigProjectSettings.state.direnv) project.getDirenv() else emptyEnv
env.findExecutableOnPATH("zls") ?: run {
return false
}
} else {
zlsPath.toNioPathOrNull() ?: run {
return false
}
}
}
if (!zlsPath.toFile().exists()) {
return false
}
if (!zlsPath.isRegularFile() || !zlsPath.isExecutable()) {
return false
}
return true
}
val Project.zlsSettings get() = service<ZLSProjectSettingsService>()

View file

@ -25,35 +25,31 @@ package com.falsepattern.zigbrains.lsp.settings
import com.falsepattern.zigbrains.lsp.config.SemanticTokens import com.falsepattern.zigbrains.lsp.config.SemanticTokens
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.util.xmlb.annotations.Attribute
import org.jetbrains.annotations.NonNls import org.jetbrains.annotations.NonNls
@Suppress("PropertyName") @Suppress("PropertyName")
data class ZLSSettings( data class ZLSSettings(
var zlsPath: @NonNls String = "", @JvmField @Attribute val zlsConfigPath: @NonNls String = "",
var zlsConfigPath: @NonNls String = "", @JvmField @Attribute val inlayHints: Boolean = true,
val inlayHints: Boolean = true, @JvmField @Attribute val enable_snippets: Boolean = true,
val enable_snippets: Boolean = true, @JvmField @Attribute val enable_argument_placeholders: Boolean = true,
val enable_argument_placeholders: Boolean = true, @JvmField @Attribute val completion_label_details: Boolean = true,
val completion_label_details: Boolean = true, @JvmField @Attribute val enable_build_on_save: Boolean = false,
val enable_build_on_save: Boolean = false, @JvmField @Attribute val build_on_save_args: String = "",
val build_on_save_args: String = "", @JvmField @Attribute val semantic_tokens: SemanticTokens = SemanticTokens.full,
val semantic_tokens: SemanticTokens = SemanticTokens.full, @JvmField @Attribute val inlay_hints_show_variable_type_hints: Boolean = true,
val inlay_hints_show_variable_type_hints: Boolean = true, @JvmField @Attribute val inlay_hints_show_struct_literal_field_type: Boolean = true,
val inlay_hints_show_struct_literal_field_type: Boolean = true, @JvmField @Attribute val inlay_hints_show_parameter_name: Boolean = true,
val inlay_hints_show_parameter_name: Boolean = true, @JvmField @Attribute val inlay_hints_show_builtin: Boolean = true,
val inlay_hints_show_builtin: Boolean = true, @JvmField @Attribute val inlay_hints_exclude_single_argument: Boolean = true,
val inlay_hints_exclude_single_argument: Boolean = true, @JvmField @Attribute val inlay_hints_hide_redundant_param_names: Boolean = false,
val inlay_hints_hide_redundant_param_names: Boolean = false, @JvmField @Attribute val inlay_hints_hide_redundant_param_names_last_token: Boolean = false,
val inlay_hints_hide_redundant_param_names_last_token: Boolean = false, @JvmField @Attribute val warn_style: Boolean = false,
val warn_style: Boolean = false, @JvmField @Attribute val highlight_global_var_declarations: Boolean = false,
val highlight_global_var_declarations: Boolean = false, @JvmField @Attribute val skip_std_references: Boolean = false,
val skip_std_references: Boolean = false, @JvmField @Attribute val prefer_ast_check_as_child_process: Boolean = true,
val prefer_ast_check_as_child_process: Boolean = true, @JvmField @Attribute val builtin_path: String? = null,
val builtin_path: String? = null, @JvmField @Attribute val build_runner_path: @NonNls String? = null,
val build_runner_path: @NonNls String? = null, @JvmField @Attribute val global_cache_path: @NonNls String? = null,
val global_cache_path: @NonNls String? = null, )
): ZigProjectConfigurationProvider.Settings {
override fun apply(project: Project) {
project.zlsSettings.loadState(this)
}
}

View file

@ -24,12 +24,13 @@ package com.falsepattern.zigbrains.lsp.settings
import com.falsepattern.zigbrains.lsp.config.ZLSConfig import com.falsepattern.zigbrains.lsp.config.ZLSConfig
import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider
import com.falsepattern.zigbrains.lsp.zls.zls
import com.falsepattern.zigbrains.shared.cli.translateCommandline import com.falsepattern.zigbrains.shared.cli.translateCommandline
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
class ZLSSettingsConfigProvider: ZLSConfigProvider { class ZLSSettingsConfigProvider: ZLSConfigProvider {
override fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig { override fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig {
val state = project.zlsSettings.state val state = project.zls?.settings ?: return previous
return previous.copy( return previous.copy(
enable_snippets = state.enable_snippets, enable_snippets = state.enable_snippets,
enable_argument_placeholders = state.enable_argument_placeholders, enable_argument_placeholders = state.enable_argument_placeholders,

View file

@ -1,57 +0,0 @@
/*
* 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.lsp.settings
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.dsl.builder.Panel
class ZLSSettingsConfigurable(private val project: Project): SubConfigurable {
private var appSettingsComponent: ZLSSettingsPanel? = null
override fun createComponent(holder: ZigProjectConfigurationProvider.SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel {
val settingsPanel = ZLSSettingsPanel(project).apply { attach(panel) }.also { Disposer.register(this, it) }
appSettingsComponent = settingsPanel
return settingsPanel
}
override fun isModified(): Boolean {
val data = appSettingsComponent?.data ?: return false
return project.zlsSettings.state != data
}
override fun apply() {
val data = appSettingsComponent?.data ?: return
val settings = project.zlsSettings
settings.state = data
}
override fun reset() {
appSettingsComponent?.data = project.zlsSettings.state
}
override fun dispose() {
appSettingsComponent = null
}
}

View file

@ -22,74 +22,27 @@
package com.falsepattern.zigbrains.lsp.settings package com.falsepattern.zigbrains.lsp.settings
import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.direnv.Env
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
import com.falsepattern.zigbrains.lsp.ZLSBundle import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.config.SemanticTokens import com.falsepattern.zigbrains.lsp.config.SemanticTokens
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.shared.cli.call
import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.execution.processTools.mapFlat
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.openapi.vfs.toNioPathOrNull
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.fields.ExtendableTextField import com.intellij.ui.components.fields.ExtendableTextField
import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.components.textFieldWithBrowseButton
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.ui.dsl.builder.Row import com.intellij.ui.dsl.builder.Row
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.annotations.PropertyKey import org.jetbrains.annotations.PropertyKey
import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString
@Suppress("PrivatePropertyName") @Suppress("PrivatePropertyName")
class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel { class ZLSSettingsPanel() : ImmutableElementPanel<ZLSSettings> {
private val zlsPath = textFieldWithBrowseButton(
project,
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
.withTitle(ZLSBundle.message("settings.zls-path.browse.title")),
).also {
it.textField.document.addDocumentListener(object: DocumentAdapter() {
override fun textChanged(p0: DocumentEvent) {
dispatchUpdateUI()
}
})
Disposer.register(this, it)
}
private val zlsConfigPath = textFieldWithBrowseButton( private val zlsConfigPath = textFieldWithBrowseButton(
project, null,
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
.withTitle(ZLSBundle.message("settings.zls-config-path.browse.title")) .withTitle(ZLSBundle.message("settings.zls-config-path.browse.title"))
).also { Disposer.register(this, it) } ).also { Disposer.register(this, it) }
private val zlsVersion = JBTextArea().also { it.isEditable = false }
private var debounce: Job? = null
private var direnv: Boolean = project.zigProjectSettings.state.direnv
private val inlayHints = JBCheckBox() private val inlayHints = JBCheckBox()
private val enable_snippets = JBCheckBox() private val enable_snippets = JBCheckBox()
private val enable_argument_placeholders = JBCheckBox() private val enable_argument_placeholders = JBCheckBox()
@ -112,18 +65,7 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
private val build_runner_path = ExtendableTextField() private val build_runner_path = ExtendableTextField()
private val global_cache_path = ExtendableTextField() private val global_cache_path = ExtendableTextField()
override fun attach(p: Panel) = with(p) { override fun attach(p: Panel): Unit = with(p) {
if (!project.isDefault) {
group(ZLSBundle.message("settings.group.title")) {
fancyRow(
"settings.zls-path.label",
"settings.zls-path.tooltip"
) {
cell(zlsPath).resizableColumn().align(AlignX.FILL)
}
row(ZLSBundle.message("settings.zls-version.label")) {
cell(zlsVersion)
}
fancyRow( fancyRow(
"settings.zls-config-path.label", "settings.zls-config-path.label",
"settings.zls-config-path.tooltip" "settings.zls-config-path.tooltip"
@ -152,7 +94,7 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
"settings.semantic_tokens.label", "settings.semantic_tokens.label",
"settings.semantic_tokens.tooltip" "settings.semantic_tokens.tooltip"
) { cell(semantic_tokens) } ) { cell(semantic_tokens) }
group(ZLSBundle.message("settings.inlay-hints-group.label")) { collapsibleGroup(ZLSBundle.message("settings.inlay-hints-group.label"), indent = false) {
fancyRow( fancyRow(
"settings.inlay-hints-enable.label", "settings.inlay-hints-enable.label",
"settings.inlay-hints-enable.tooltip" "settings.inlay-hints-enable.tooltip"
@ -215,18 +157,21 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
"settings.global_cache_path.tooltip" "settings.global_cache_path.tooltip"
) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) } ) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) }
} }
}
dispatchAutodetect(false) override fun isModified(elem: ZLSSettings): Boolean {
return elem != data
} }
override fun direnvChanged(state: Boolean) { override fun apply(elem: ZLSSettings): ZLSSettings? {
direnv = state return data
dispatchAutodetect(true)
} }
override var data override fun reset(elem: ZLSSettings?) {
get() = if (project.isDefault) ZLSSettings() else ZLSSettings( data = elem ?: ZLSSettings()
zlsPath.text, }
private var data
get() = ZLSSettings(
zlsConfigPath.text, zlsConfigPath.text,
inlayHints.isSelected, inlayHints.isSelected,
enable_snippets.isSelected, enable_snippets.isSelected,
@ -251,7 +196,6 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
global_cache_path.text?.ifBlank { null }, global_cache_path.text?.ifBlank { null },
) )
set(value) { set(value) {
zlsPath.text = value.zlsPath
zlsConfigPath.text = value.zlsConfigPath zlsConfigPath.text = value.zlsConfigPath
inlayHints.isSelected = value.inlayHints inlayHints.isSelected = value.inlayHints
enable_snippets.isSelected = value.enable_snippets enable_snippets.isSelected = value.enable_snippets
@ -275,72 +219,10 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
builtin_path.text = value.builtin_path ?: "" builtin_path.text = value.builtin_path ?: ""
build_runner_path.text = value.build_runner_path ?: "" build_runner_path.text = value.build_runner_path ?: ""
global_cache_path.text = value.global_cache_path ?: "" global_cache_path.text = value.global_cache_path ?: ""
dispatchUpdateUI()
}
private fun dispatchAutodetect(force: Boolean) {
project.zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) {
withModalProgress(ModalTaskOwner.component(zlsPath), "Detecting ZLS...", TaskCancellation.cancellable()) {
autodetect(force)
}
}
}
suspend fun autodetect(force: Boolean) {
if (force || zlsPath.text.isBlank()) {
getDirenv().findExecutableOnPATH("zls")?.let {
if (force || zlsPath.text.isBlank()) {
zlsPath.text = it.pathString
dispatchUpdateUI()
}
}
}
} }
override fun dispose() { override fun dispose() {
debounce?.cancel("Disposed") zlsConfigPath.dispose()
}
private suspend fun getDirenv(): Env {
if (!project.isDefault && DirenvCmd.direnvInstalled() && direnv)
return project.getDirenv()
return emptyEnv
}
private fun dispatchUpdateUI() {
debounce?.cancel("New debounce")
debounce = project.zigCoroutineScope.launch {
updateUI()
}
}
private suspend fun updateUI() {
if (project.isDefault)
return
delay(200)
val zlsPath = this.zlsPath.text.ifBlank { null }?.toNioPathOrNull()
if (zlsPath == null) {
withEDTContext(ModalityState.any()) {
zlsVersion.text = "[zls path empty or invalid]"
}
return
}
val workingDir = project.guessProjectDir()?.toNioPathOrNull()
val result = createCommandLineSafe(workingDir, zlsPath, "version")
.map { it.withEnvironment(getDirenv().env) }
.mapFlat { it.call() }
.getOrElse { throwable ->
throwable.printStackTrace()
withEDTContext(ModalityState.any()) {
zlsVersion.text = "[failed to run \"zls version\"]\n${throwable.message}"
}
return
}
val version = result.stdout.trim()
withEDTContext(ModalityState.any()) {
zlsVersion.text = version
zlsVersion.foreground = JBColor.foreground()
}
} }
} }

View file

@ -0,0 +1,88 @@
/*
* 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.lsp.zls
import com.intellij.openapi.ui.NamedConfigurable
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.NlsSafe
import com.intellij.ui.dsl.builder.panel
import java.awt.Dimension
import java.util.UUID
import javax.swing.JComponent
class ZLSConfigurable(val uuid: UUID, zls: ZLSVersion): NamedConfigurable<UUID>() {
var zls: ZLSVersion = zls
set(value) {
zlsInstallations[uuid] = value
field = value
}
private var myView: ZLSPanel? = null
override fun setDisplayName(name: String?) {
zls = zls.copy(name = name)
}
override fun getEditableObject(): UUID? {
return uuid
}
override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? {
return displayName
}
override fun createOptionsPanel(): JComponent? {
var view = myView
if (view == null) {
view = ZLSPanel()
view.reset(zls)
myView = view
}
val p = panel {
view.attach(this@panel)
}
p.preferredSize = Dimension(640, 480)
return p
}
override fun getDisplayName(): @NlsContexts.ConfigurableName String? {
return zls.name
}
override fun isModified(): Boolean {
return myView?.isModified(zls) == true
}
override fun apply() {
myView?.apply(zls)?.let { zls = it }
}
override fun reset() {
myView?.reset(zls)
}
override fun disposeUIResources() {
myView?.dispose()
myView = null
super.disposeUIResources()
}
}

View file

@ -0,0 +1,51 @@
/*
* 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.lsp.zls
import com.falsepattern.zigbrains.lsp.zls.ZLSInstallationsService.MyState
import com.falsepattern.zigbrains.shared.UUIDMapSerializable
import com.falsepattern.zigbrains.shared.UUIDStorage
import com.intellij.openapi.components.*
@Service(Service.Level.APP)
@State(
name = "ZLSInstallations",
storages = [Storage("zigbrains.xml")]
)
class ZLSInstallationsService: UUIDMapSerializable.Converting<ZLSVersion, ZLSVersion.Ref, MyState>(MyState()) {
override fun serialize(value: ZLSVersion) = value.toRef()
override fun deserialize(value: ZLSVersion.Ref) = value.resolve()
override fun getStorage(state: MyState) = state.zlsInstallations
override fun updateStorage(state: MyState, storage: ZLSStorage) = state.copy(zlsInstallations = storage)
data class MyState(@JvmField val zlsInstallations: ZLSStorage = emptyMap())
companion object {
@JvmStatic
fun getInstance(): ZLSInstallationsService = service<ZLSInstallationsService>()
}
}
inline val zlsInstallations: ZLSInstallationsService get() = ZLSInstallationsService.getInstance()
private typealias ZLSStorage = UUIDStorage<ZLSVersion.Ref>

View file

@ -0,0 +1,141 @@
/*
* 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.lsp.zls
import com.falsepattern.zigbrains.lsp.settings.ZLSSettingsPanel
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.ui.ImmutableNamedElementPanelBase
import com.falsepattern.zigbrains.shared.cli.call
import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.textFieldWithBrowseButton
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.swing.event.DocumentEvent
import kotlin.io.path.pathString
class ZLSPanel() : ImmutableNamedElementPanelBase<ZLSVersion>() {
private val pathToZLS = textFieldWithBrowseButton(
null,
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor().withTitle("Path to the zls executable")
).also {
it.textField.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
dispatchUpdateUI()
}
})
Disposer.register(this, it)
}
private val zlsVersion = JBTextArea().also { it.isEditable = false }
private var settingsPanel: ZLSSettingsPanel? = null
private var debounce: Job? = null
override fun attach(p: Panel): Unit = with(p) {
super.attach(p)
row("Path:") {
cell(pathToZLS).resizableColumn().align(AlignX.FILL)
}
row("Version:") {
cell(zlsVersion)
}
val sp = ZLSSettingsPanel()
p.collapsibleGroup("Settings", indent = false) {
sp.attach(this@collapsibleGroup)
}
settingsPanel = sp
}
override fun isModified(version: ZLSVersion): Boolean {
val name = nameFieldValue ?: return false
val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return false
return name != version.name || version.path != path || settingsPanel?.isModified(version.settings) == true
}
override fun apply(version: ZLSVersion): ZLSVersion? {
val path = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull() ?: return null
return version.copy(path = path, name = nameFieldValue ?: "", settings = settingsPanel?.apply(version.settings) ?: version.settings)
}
override fun reset(version: ZLSVersion?) {
nameFieldValue = version?.name ?: ""
this.pathToZLS.text = version?.path?.pathString ?: ""
settingsPanel?.reset(version?.settings)
dispatchUpdateUI()
}
private fun dispatchUpdateUI() {
debounce?.cancel("New debounce")
debounce = zigCoroutineScope.launch {
updateUI()
}
}
private suspend fun updateUI() {
delay(200)
val pathToZLS = this.pathToZLS.text.ifBlank { null }?.toNioPathOrNull()
if (pathToZLS == null) {
withEDTContext(ModalityState.any()) {
zlsVersion.text = "[zls path empty or invalid]"
}
return
}
val versionCommand = createCommandLineSafe(null, pathToZLS, "--version").getOrElse {
it.printStackTrace()
withEDTContext(ModalityState.any()) {
zlsVersion.text = "[could not create \"zls --version\" command]\n${it.message}"
}
return
}
val result = versionCommand.call().getOrElse {
it.printStackTrace()
withEDTContext(ModalityState.any()) {
zlsVersion.text = "[failed to run \"zls --version\"]\n${it.message}"
}
return
}
val version = result.stdout.trim()
withEDTContext(ModalityState.any()) {
zlsVersion.text = version
zlsVersion.foreground = JBColor.foreground()
}
}
override fun dispose() {
debounce?.cancel("Disposed")
settingsPanel?.dispose()
settingsPanel = null
}
}

View file

@ -20,24 +20,26 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>. * along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/ */
package com.falsepattern.zigbrains.project.settings package com.falsepattern.zigbrains.lsp.zls
import com.falsepattern.zigbrains.shared.SubConfigurable import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.base.withExtraData
import com.falsepattern.zigbrains.shared.asString
import com.falsepattern.zigbrains.shared.asUUID
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager import java.util.UUID
class ZigCoreProjectConfigurationProvider: ZigProjectConfigurationProvider { fun <T: ZigToolchain> T.withZLS(uuid: UUID?): T {
override fun handleMainConfigChanged(project: Project) { return withExtraData("zls_uuid", uuid?.asString())
} }
override fun createConfigurable(project: Project): SubConfigurable { val ZigToolchain.zlsUUID: UUID? get() {
return ZigProjectConfigurable(project) return extraData["zls_uuid"]?.asUUID()
} }
override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel { val ZigToolchain.zls: ZLSVersion? get() {
return ZigProjectSettingsPanel(holder, ProjectManager.getInstance().defaultProject) return zlsUUID?.let { zlsInstallations[it] }
} }
override val priority: Int val Project.zls: ZLSVersion? get() = ZigToolchainService.getInstance(this).toolchain?.zls
get() = 0
}

View file

@ -0,0 +1,91 @@
/*
* 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.lsp.zls
import com.falsepattern.zigbrains.lsp.settings.ZLSSettings
import com.falsepattern.zigbrains.shared.NamedObject
import com.falsepattern.zigbrains.shared.cli.call
import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.text.SemVer
import java.nio.file.Path
import com.intellij.util.xmlb.annotations.Attribute
import kotlin.io.path.isDirectory
import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile
import kotlin.io.path.pathString
data class ZLSVersion(val path: Path, override val name: String? = null, val settings: ZLSSettings = ZLSSettings()): NamedObject<ZLSVersion> {
override fun withName(newName: String?): ZLSVersion {
return copy(name = newName)
}
fun toRef(): Ref {
return Ref(path.pathString, name, settings)
}
fun isValid(): Boolean {
if (!path.toFile().exists())
return false
if (!path.isRegularFile() || !path.isExecutable())
return false
return true
}
suspend fun version(): SemVer? {
if (!isValid())
return null
val cli = createCommandLineSafe(null, path, "--version").getOrElse { return null }
val info = cli.call(5000).getOrElse { return null }
return SemVer.parseFromText(info.stdout.trim())
}
companion object {
suspend fun tryFromPath(path: Path): ZLSVersion? {
var zls = ZLSVersion(path)
if (!zls.isValid())
return null
val version = zls.version()?.rawVersion
if (version != null) {
zls = zls.copy(name = "ZLS $version")
}
return zls
}
}
data class Ref(
@JvmField
@Attribute
val path: String? = "",
@JvmField
@Attribute
val name: String? = "",
@JvmField
val settings: ZLSSettings = ZLSSettings()
) {
fun resolve(): ZLSVersion? {
return path?.ifBlank { null }?.toNioPathOrNull()?.let { ZLSVersion(it, name, settings) }
}
}
}

View file

@ -0,0 +1,48 @@
/*
* 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.lsp.zls.downloader
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.lsp.zls.ui.getSuggestedZLSPath
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.IUserDataBridge
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
import com.falsepattern.zigbrains.shared.downloader.Downloader
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.util.system.OS
import java.awt.Component
import java.nio.file.Path
import kotlin.io.path.isDirectory
class ZLSDownloader(component: Component, private val data: IUserDataBridge?) : Downloader<ZLSVersion, ZLSVersionInfo>(component) {
override val windowTitle get() = "Install ZLS"
override val versionInfoFetchTitle get() = "Fetching zls version information"
override fun downloadProgressTitle(version: ZLSVersionInfo) = "Installing ZLS ${version.version.rawVersion}"
override fun localSelector() = ZLSLocalSelector(component)
override suspend fun downloadVersionList(): List<ZLSVersionInfo> {
val toolchain = data?.getUserData(ZigToolchainConfigurable.TOOLCHAIN_KEY)?.get()
val project = data?.getUserData(ZigProjectConfigurationProvider.PROJECT_KEY)
return ZLSVersionInfo.downloadVersionInfoFor(toolchain, project)
}
override fun getSuggestedPath() = getSuggestedZLSPath()
}

Some files were not shown because too many files have changed in this diff Show more