feat!: Toolchain rework
This commit is contained in:
commit
d5359e5816
111 changed files with 4980 additions and 1362 deletions
8
LICENSE
8
LICENSE
|
@ -25,6 +25,11 @@ which are the property of the Zig Software Foundation.
|
|||
(https://github.com/ziglang/logo)
|
||||
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,
|
||||
developed by HTGAzureX1212 (https://github.com/HTGAzureX1212), licensed under the Apache 2.0 license.
|
||||
--------------------------------
|
||||
|
@ -37,4 +42,5 @@ All of the licenses listed here are available in the following files, bundled wi
|
|||
- licenses/CC_BY_SA_4.0.LICENSE
|
||||
- licenses/GPL3.LICENSE
|
||||
- licenses/INTELLIJ-RUST.LICENSE
|
||||
- licenses/LGPL3.LICENSE
|
||||
- licenses/LGPL3.LICENSE
|
||||
- licenses/ZLS.LICENSE
|
|
@ -24,14 +24,14 @@ package com.falsepattern.zigbrains.debugger.execution.binary
|
|||
|
||||
import com.falsepattern.zigbrains.debugger.ZigDebugBundle
|
||||
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.configurations.GeneralCommandLine
|
||||
import com.intellij.execution.runners.ExecutionEnvironment
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
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 cfg = configuration
|
||||
cfg.workingDirectory.path?.let { cli.withWorkingDirectory(it) }
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
package com.falsepattern.zigbrains.debugger.runner.base
|
||||
|
||||
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.intellij.execution.configurations.GeneralCommandLine
|
||||
import com.intellij.execution.configurations.PtyCommandLine
|
||||
|
@ -34,7 +34,7 @@ import java.io.File
|
|||
|
||||
class ZigDebugEmitBinaryInstaller<ProfileState: ZigProfileState<*>>(
|
||||
private val profileState: ProfileState,
|
||||
private val toolchain: AbstractZigToolchain,
|
||||
private val toolchain: ZigToolchain,
|
||||
private val executableFile: File,
|
||||
private val exeArgs: List<String>
|
||||
): Installer {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
package com.falsepattern.zigbrains.debugger.runner.base
|
||||
|
||||
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.jetbrains.cidr.ArchitectureType
|
||||
import com.jetbrains.cidr.execution.RunParameters
|
||||
|
@ -31,7 +31,7 @@ import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
|
|||
|
||||
abstract class ZigDebugParametersBase<ProfileState: ZigProfileState<*>>(
|
||||
private val driverConfiguration: DebuggerDriverConfiguration,
|
||||
protected val toolchain: AbstractZigToolchain,
|
||||
protected val toolchain: ZigToolchain,
|
||||
protected val profileState: ProfileState
|
||||
): RunParameters() {
|
||||
override fun getDebuggerDriverConfiguration(): DebuggerDriverConfiguration {
|
||||
|
|
|
@ -24,7 +24,7 @@ package com.falsepattern.zigbrains.debugger.runner.base
|
|||
|
||||
import com.falsepattern.zigbrains.debugger.ZigDebugBundle
|
||||
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.openapi.util.io.FileUtil
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
|
@ -39,7 +39,7 @@ import kotlin.io.path.isExecutable
|
|||
|
||||
abstract class ZigDebugParametersEmitBinaryBase<ProfileState: ZigProfileState<*>>(
|
||||
driverConfiguration: DebuggerDriverConfiguration,
|
||||
toolchain: AbstractZigToolchain,
|
||||
toolchain: ZigToolchain,
|
||||
profileState: ProfileState,
|
||||
) : ZigDebugParametersBase<ProfileState>(driverConfiguration, toolchain, profileState), PreLaunchAware {
|
||||
@Volatile
|
||||
|
|
|
@ -26,7 +26,7 @@ import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProv
|
|||
import com.falsepattern.zigbrains.debugger.ZigLocalDebugProcess
|
||||
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
|
||||
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.withEDTContext
|
||||
import com.intellij.execution.DefaultExecutionResult
|
||||
|
@ -52,7 +52,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
|
|||
@Throws(ExecutionException::class)
|
||||
override suspend fun execute(
|
||||
state: ProfileState,
|
||||
toolchain: AbstractZigToolchain,
|
||||
toolchain: ZigToolchain,
|
||||
environment: ExecutionEnvironment
|
||||
): RunContentDescriptor? {
|
||||
val project = environment.project
|
||||
|
@ -67,7 +67,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
|
|||
@Throws(ExecutionException::class)
|
||||
private suspend fun executeWithDriver(
|
||||
state: ProfileState,
|
||||
toolchain: AbstractZigToolchain,
|
||||
toolchain: ZigToolchain,
|
||||
environment: ExecutionEnvironment,
|
||||
debuggerDriver: DebuggerDriverConfiguration
|
||||
): RunContentDescriptor? {
|
||||
|
@ -113,7 +113,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
|
|||
protected abstract fun getDebugParameters(
|
||||
state: ProfileState,
|
||||
debuggerDriver: DebuggerDriverConfiguration,
|
||||
toolchain: AbstractZigToolchain
|
||||
toolchain: ZigToolchain
|
||||
): ZigDebugParametersBase<ProfileState>
|
||||
|
||||
private class SharedConsoleBuilder(private val console: ConsoleView) : TextConsoleBuilder() {
|
||||
|
|
|
@ -26,13 +26,13 @@ import com.falsepattern.zigbrains.debugger.ZigDebugBundle
|
|||
import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinary
|
||||
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller
|
||||
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.jetbrains.cidr.execution.Installer
|
||||
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) {
|
||||
private val executableFile = profileState.configuration.exePath.path?.toFile() ?: throw ExecutionException(ZigDebugBundle.message("exception.missing-exe-path"))
|
||||
override fun getInstaller(): Installer {
|
||||
|
|
|
@ -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.ZigDebugRunnerBase
|
||||
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
|
||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
|
||||
import com.intellij.execution.configurations.RunProfile
|
||||
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
|
||||
|
||||
|
@ -36,7 +36,7 @@ class ZigDebugRunnerBinary: ZigDebugRunnerBase<ZigProfileStateBinary>() {
|
|||
override fun getDebugParameters(
|
||||
state: ZigProfileStateBinary,
|
||||
debuggerDriver: DebuggerDriverConfiguration,
|
||||
toolchain: AbstractZigToolchain
|
||||
toolchain: ZigToolchain
|
||||
): ZigDebugParametersBase<ZigProfileStateBinary> {
|
||||
return ZigDebugParametersBinary(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
|
||||
}
|
||||
|
|
|
@ -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.ZigDebugParametersBase
|
||||
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.openapi.util.SystemInfo
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
|
@ -46,7 +46,7 @@ import kotlin.io.path.isRegularFile
|
|||
|
||||
class ZigDebugParametersBuild(
|
||||
driverConfiguration: DebuggerDriverConfiguration,
|
||||
toolchain: AbstractZigToolchain,
|
||||
toolchain: ZigToolchain,
|
||||
profileState: ZigProfileStateBuild
|
||||
) : ZigDebugParametersBase<ZigProfileStateBuild>(driverConfiguration, toolchain, profileState), PreLaunchAware {
|
||||
@Volatile
|
||||
|
|
|
@ -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.build.ZigExecConfigBuild
|
||||
import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild
|
||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
|
||||
import com.intellij.execution.configurations.RunProfile
|
||||
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
|
||||
|
||||
|
@ -36,7 +36,7 @@ class ZigDebugRunnerBuild: ZigDebugRunnerBase<ZigProfileStateBuild>() {
|
|||
override fun getDebugParameters(
|
||||
state: ZigProfileStateBuild,
|
||||
debuggerDriver: DebuggerDriverConfiguration,
|
||||
toolchain: AbstractZigToolchain
|
||||
toolchain: ZigToolchain
|
||||
): ZigDebugParametersBase<ZigProfileStateBuild> {
|
||||
return ZigDebugParametersBuild(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
|
||||
}
|
||||
|
|
|
@ -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.ZigDebugParametersEmitBinaryBase
|
||||
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.debugger.backend.DebuggerDriverConfiguration
|
||||
|
||||
class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateRun) :
|
||||
class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateRun) :
|
||||
ZigDebugParametersEmitBinaryBase<ZigProfileStateRun>(driverConfiguration, toolchain, profileState) {
|
||||
override fun getInstaller(): Installer {
|
||||
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit())
|
||||
|
|
|
@ -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.run.ZigExecConfigRun
|
||||
import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun
|
||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
|
||||
import com.intellij.execution.configurations.RunProfile
|
||||
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
|
||||
|
||||
|
@ -36,7 +36,7 @@ class ZigDebugRunnerRun: ZigDebugRunnerBase<ZigProfileStateRun>() {
|
|||
override fun getDebugParameters(
|
||||
state: ZigProfileStateRun,
|
||||
debuggerDriver: DebuggerDriverConfiguration,
|
||||
toolchain: AbstractZigToolchain
|
||||
toolchain: ZigToolchain
|
||||
): ZigDebugParametersBase<ZigProfileStateRun> {
|
||||
return ZigDebugParametersRun(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
|
||||
}
|
||||
|
|
|
@ -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.ZigDebugParametersEmitBinaryBase
|
||||
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.debugger.backend.DebuggerDriverConfiguration
|
||||
|
||||
class ZigDebugParametersTest(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateTest) :
|
||||
class ZigDebugParametersTest(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateTest) :
|
||||
ZigDebugParametersEmitBinaryBase<ZigProfileStateTest>(driverConfiguration, toolchain, profileState) {
|
||||
override fun getInstaller(): Installer {
|
||||
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, listOf())
|
||||
|
|
|
@ -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.test.ZigExecConfigTest
|
||||
import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest
|
||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
|
||||
import com.intellij.execution.configurations.RunProfile
|
||||
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
|
||||
|
||||
|
@ -36,7 +36,7 @@ class ZigDebugRunnerTest: ZigDebugRunnerBase<ZigProfileStateTest>() {
|
|||
override fun getDebugParameters(
|
||||
state: ZigProfileStateTest,
|
||||
debuggerDriver: DebuggerDriverConfiguration,
|
||||
toolchain: AbstractZigToolchain
|
||||
toolchain: ZigToolchain
|
||||
): ZigDebugParametersBase<ZigProfileStateTest> {
|
||||
return ZigDebugParametersTest(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
package com.falsepattern.zigbrains.debugger.toolchain
|
||||
|
||||
import com.falsepattern.zigbrains.debugger.ZigDebugBundle
|
||||
import com.falsepattern.zigbrains.shared.Unarchiver
|
||||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationType
|
||||
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.util.SystemInfo
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.intellij.platform.util.progress.reportSequentialProgress
|
||||
import com.intellij.ui.BrowserHyperlinkListener
|
||||
import com.intellij.ui.HyperlinkLabel
|
||||
import com.intellij.ui.components.JBPanel
|
||||
import com.intellij.util.application
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.download.DownloadableFileService
|
||||
import com.intellij.util.io.Decompressor
|
||||
import com.intellij.util.system.CpuArch
|
||||
import com.intellij.util.system.OS
|
||||
import com.jetbrains.cidr.execution.debugger.CidrDebuggerPathManager
|
||||
|
@ -168,7 +169,9 @@ class ZigDebuggerToolchainService {
|
|||
}
|
||||
|
||||
try {
|
||||
downloadAndUnArchive(baseDir, downloadableBinaries)
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadAndUnArchive(baseDir, downloadableBinaries)
|
||||
}
|
||||
return DownloadResult.Ok(baseDir)
|
||||
} catch (e: IOException) {
|
||||
//TODO logging
|
||||
|
@ -207,34 +210,40 @@ class ZigDebuggerToolchainService {
|
|||
@Throws(IOException::class)
|
||||
@RequiresEdt
|
||||
private suspend fun downloadAndUnArchive(baseDir: Path, binariesToDownload: List<DownloadableDebuggerBinary>) {
|
||||
val service = DownloadableFileService.getInstance()
|
||||
reportSequentialProgress { reporter ->
|
||||
val service = DownloadableFileService.getInstance()
|
||||
|
||||
val downloadDir = baseDir.toFile()
|
||||
downloadDir.deleteRecursively()
|
||||
val downloadDir = baseDir.toFile()
|
||||
downloadDir.deleteRecursively()
|
||||
|
||||
val descriptions = binariesToDownload.map {
|
||||
service.createFileDescription(it.url, fileName(it.url))
|
||||
}
|
||||
|
||||
val downloader = service.createDownloader(descriptions, "Debugger downloading")
|
||||
val downloadDirectory = downloadPath().toFile()
|
||||
val downloadResults = withContext(Dispatchers.IO) {
|
||||
coroutineToIndicator {
|
||||
downloader.download(downloadDirectory)
|
||||
val descriptions = binariesToDownload.map {
|
||||
service.createFileDescription(it.url, fileName(it.url))
|
||||
}
|
||||
}
|
||||
val versions = Properties()
|
||||
for (result in downloadResults) {
|
||||
val downloadUrl = result.second.downloadUrl
|
||||
val binaryToDownload = binariesToDownload.first { it.url == downloadUrl }
|
||||
val propertyName = binaryToDownload.propertyName
|
||||
val archiveFile = result.first
|
||||
Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix)
|
||||
archiveFile.delete()
|
||||
versions[propertyName] = binaryToDownload.version
|
||||
}
|
||||
|
||||
saveVersionsFile(baseDir, versions)
|
||||
val downloader = service.createDownloader(descriptions, "Debugger downloading")
|
||||
val downloadDirectory = downloadPath().toFile()
|
||||
val downloadResults = reporter.sizedStep(100) {
|
||||
coroutineToIndicator {
|
||||
downloader.download(downloadDirectory)
|
||||
}
|
||||
}
|
||||
val versions = Properties()
|
||||
for (result in downloadResults) {
|
||||
val downloadUrl = result.second.downloadUrl
|
||||
val binaryToDownload = binariesToDownload.first { it.url == downloadUrl }
|
||||
val propertyName = binaryToDownload.propertyName
|
||||
val archiveFile = result.first
|
||||
reporter.indeterminateStep {
|
||||
coroutineToIndicator {
|
||||
Unarchiver.unarchive(archiveFile.toPath(), baseDir, binaryToDownload.prefix)
|
||||
}
|
||||
}
|
||||
archiveFile.delete()
|
||||
versions[propertyName] = binaryToDownload.version
|
||||
}
|
||||
|
||||
saveVersionsFile(baseDir, versions)
|
||||
}
|
||||
}
|
||||
|
||||
private fun lldbUrls(): Pair<URL, URL>? {
|
||||
|
@ -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 {
|
||||
class Ok(val baseDir: Path): DownloadResult()
|
||||
data object NoUrls: DownloadResult()
|
||||
|
|
|
@ -22,10 +22,6 @@
|
|||
|
||||
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.plugins.PluginManager
|
||||
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.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
import java.lang.reflect.Constructor
|
||||
import java.lang.reflect.Method
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
class ZBStartup: ProjectActivity {
|
||||
var firstInit = true
|
||||
|
@ -73,19 +67,6 @@ class ZBStartup: ProjectActivity {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,23 +29,53 @@ import com.intellij.ide.impl.isTrusted
|
|||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationType
|
||||
import com.intellij.notification.Notifications
|
||||
import com.intellij.openapi.components.*
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectDir
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.UserDataHolder
|
||||
import com.intellij.openapi.vfs.toNioPathOrNull
|
||||
import com.intellij.platform.util.progress.withProgressText
|
||||
import com.intellij.util.io.awaitExit
|
||||
import com.intellij.util.xmlb.annotations.Attribute
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.isRegularFile
|
||||
|
||||
object DirenvCmd {
|
||||
suspend fun importDirenv(project: Project): Env {
|
||||
if (!direnvInstalled() || !project.isTrusted())
|
||||
return emptyEnv
|
||||
val workDir = project.guessProjectDir()?.toNioPath() ?: return emptyEnv
|
||||
@Service(Service.Level.PROJECT)
|
||||
@State(
|
||||
name = "Direnv",
|
||||
storages = [Storage("zigbrains.xml")]
|
||||
)
|
||||
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.output.contains("is blocked")) {
|
||||
Notifications.Bus.notify(Notification(
|
||||
|
@ -54,7 +84,7 @@ object DirenvCmd {
|
|||
ZigBrainsBundle.message("notification.content.direnv-blocked"),
|
||||
NotificationType.ERROR
|
||||
))
|
||||
return emptyEnv
|
||||
return Env.empty
|
||||
} else {
|
||||
Notifications.Bus.notify(Notification(
|
||||
GROUP_DISPLAY_ID,
|
||||
|
@ -62,22 +92,22 @@ object DirenvCmd {
|
|||
ZigBrainsBundle.message("notification.content.direnv-error", runOutput.output),
|
||||
NotificationType.ERROR
|
||||
))
|
||||
return emptyEnv
|
||||
return Env.empty
|
||||
}
|
||||
}
|
||||
return if (runOutput.output.isBlank()) {
|
||||
emptyEnv
|
||||
Env.empty
|
||||
} else {
|
||||
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 (process, exitCode) = withProgressText("Running ${cli.commandLineString}") {
|
||||
withContext(Dispatchers.IO) {
|
||||
project.direnvService.mutex.withLock {
|
||||
mutex.withLock {
|
||||
val process = cli.createProcess()
|
||||
val exitCode = process.awaitExit()
|
||||
process to exitCode
|
||||
|
@ -94,17 +124,39 @@ object DirenvCmd {
|
|||
return DirenvOutput(stdOut, false)
|
||||
}
|
||||
|
||||
private const val GROUP_DISPLAY_ID = "zigbrains-direnv"
|
||||
|
||||
private val _direnvInstalled by lazy {
|
||||
// Using the builtin stuff here instead of Env because it should only scan for direnv on the process path
|
||||
PathEnvironmentVariableUtil.findExecutableInPathOnAnyOS("direnv") != null
|
||||
fun hasDotEnv(): Boolean {
|
||||
if (!isInstalled)
|
||||
return false
|
||||
val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return false
|
||||
return envFiles.any { projectDir.resolve(it).isRegularFile() }
|
||||
}
|
||||
|
||||
data class State(
|
||||
@JvmField
|
||||
@Attribute
|
||||
var enabled: DirenvState = DirenvState.Auto
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val GROUP_DISPLAY_ID = "zigbrains-direnv"
|
||||
fun getInstance(project: Project): IDirenvService = project.service<DirenvService>()
|
||||
|
||||
private val STATE_KEY = Key.create<DirenvState>("DIRENV_STATE")
|
||||
|
||||
fun getStateFor(data: UserDataHolder?, project: Project?): DirenvState {
|
||||
return data?.getUserData(STATE_KEY) ?: project?.let { getInstance(project).isEnabled } ?: DirenvState.Disabled
|
||||
}
|
||||
|
||||
fun setStateFor(data: UserDataHolder, state: DirenvState) {
|
||||
data.putUserData(STATE_KEY, state)
|
||||
}
|
||||
}
|
||||
fun direnvInstalled() = _direnvInstalled
|
||||
}
|
||||
|
||||
suspend fun Project?.getDirenv(): Env {
|
||||
if (this == null)
|
||||
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")
|
|
@ -22,14 +22,19 @@
|
|||
|
||||
package com.falsepattern.zigbrains.direnv
|
||||
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
|
||||
@Service(Service.Level.PROJECT)
|
||||
class DirenvProjectService {
|
||||
val mutex = Mutex()
|
||||
}
|
||||
enum class DirenvState {
|
||||
Auto,
|
||||
Enabled,
|
||||
Disabled;
|
||||
|
||||
val Project.direnvService get() = service<DirenvProjectService>()
|
||||
fun isEnabled(project: Project?): Boolean {
|
||||
return when(this) {
|
||||
Enabled -> true
|
||||
Disabled -> false
|
||||
Auto -> project?.service<DirenvService>()?.hasDotEnv() == true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,8 +25,10 @@ package com.falsepattern.zigbrains.direnv
|
|||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.intellij.util.EnvironmentUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import org.jetbrains.annotations.NonNls
|
||||
import java.io.File
|
||||
import kotlin.io.path.absolute
|
||||
|
@ -34,14 +36,13 @@ import kotlin.io.path.isDirectory
|
|||
import kotlin.io.path.isExecutable
|
||||
import kotlin.io.path.isRegularFile
|
||||
|
||||
@JvmRecord
|
||||
data class Env(val env: Map<String, String>) {
|
||||
private val path get() = getVariable("PATH")?.split(File.pathSeparatorChar)
|
||||
|
||||
private fun getVariable(name: @NonNls String) =
|
||||
env.getOrElse(name) { EnvironmentUtil.getValue(name) }
|
||||
|
||||
suspend fun findExecutableOnPATH(exe: @NonNls String) = findAllExecutablesOnPATH(exe).firstOrNull()
|
||||
|
||||
fun findAllExecutablesOnPATH(exe: @NonNls String) = flow {
|
||||
val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe
|
||||
val paths = path ?: return@flow
|
||||
|
@ -54,7 +55,9 @@ data class Env(val env: Map<String, String>) {
|
|||
continue
|
||||
emit(exePath)
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
companion object {
|
||||
val empty = Env(emptyMap())
|
||||
}
|
||||
}
|
||||
|
||||
val emptyEnv = Env(emptyMap())
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -24,15 +24,12 @@ package com.falsepattern.zigbrains.project.execution.base
|
|||
|
||||
import com.falsepattern.zigbrains.ZigBrainsBundle
|
||||
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.element.*
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.options.SettingsEditor
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.ComboBox
|
||||
import com.intellij.openapi.ui.TextBrowseFolderListener
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
|
|
|
@ -22,9 +22,7 @@
|
|||
|
||||
package com.falsepattern.zigbrains.project.execution.base
|
||||
|
||||
import com.falsepattern.zigbrains.ZigBrainsBundle
|
||||
import com.falsepattern.zigbrains.direnv.DirenvCmd
|
||||
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
|
||||
import com.falsepattern.zigbrains.direnv.DirenvService
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.Executor
|
||||
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 {
|
||||
if (project.zigProjectSettings.state.direnv) {
|
||||
commandLine.withEnvironment(DirenvCmd.importDirenv(project).env)
|
||||
val direnv = DirenvService.getInstance(project)
|
||||
if (direnv.isEnabled.isEnabled(project)) {
|
||||
commandLine.withEnvironment(direnv.import().env)
|
||||
}
|
||||
return commandLine
|
||||
}
|
||||
|
|
|
@ -24,28 +24,19 @@ package com.falsepattern.zigbrains.project.execution.base
|
|||
|
||||
import com.falsepattern.zigbrains.ZigBrainsBundle
|
||||
import com.falsepattern.zigbrains.project.execution.ZigConsoleBuilder
|
||||
import com.falsepattern.zigbrains.project.run.ZigProcessHandler
|
||||
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
|
||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess
|
||||
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.configurations.CommandLineState
|
||||
import com.intellij.execution.configurations.GeneralCommandLine
|
||||
import com.intellij.execution.configurations.PtyCommandLine
|
||||
import com.intellij.execution.filters.TextConsoleBuilder
|
||||
import com.intellij.execution.process.ProcessHandler
|
||||
import com.intellij.execution.process.ProcessTerminatedListener
|
||||
import com.intellij.execution.runners.ExecutionEnvironment
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.ide.progress.ModalTaskOwner
|
||||
import com.intellij.terminal.TerminalExecutionConsole
|
||||
import com.intellij.util.system.OS
|
||||
import kotlin.collections.contains
|
||||
import com.intellij.platform.util.progress.reportProgress
|
||||
import com.intellij.platform.util.progress.reportRawProgress
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
abstract class ZigProfileState<T: ZigExecConfig<T>> (
|
||||
|
@ -66,12 +57,12 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
|
|||
|
||||
@Throws(ExecutionException::class)
|
||||
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)
|
||||
}
|
||||
|
||||
@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 zigExePath = toolchain.zig.path()
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ class ZigModuleBuilder: ModuleBuilder() {
|
|||
|
||||
override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?): ModuleWizardStep? {
|
||||
val step = ZigModuleWizardStep(parentDisposable)
|
||||
parentDisposable?.let { Disposer.register(it, step.peer) }
|
||||
parentDisposable?.let { Disposer.register(it) { step.peer.dispose() } }
|
||||
return step
|
||||
}
|
||||
|
||||
|
@ -65,14 +65,14 @@ class ZigModuleBuilder: ModuleBuilder() {
|
|||
}
|
||||
|
||||
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 {
|
||||
return peer.myComponent.withBorder()
|
||||
}
|
||||
|
||||
override fun disposeUIResources() {
|
||||
Disposer.dispose(peer)
|
||||
Disposer.dispose(peer.newProjectPanel)
|
||||
}
|
||||
|
||||
override fun updateDataModel() {
|
||||
|
|
|
@ -39,9 +39,9 @@ import com.intellij.util.ui.JBUI
|
|||
import javax.swing.JList
|
||||
import javax.swing.ListSelectionModel
|
||||
|
||||
class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProjectConfigurationProvider.SettingsPanelHolder {
|
||||
class ZigNewProjectPanel(private var handleGit: Boolean): Disposable {
|
||||
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 {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
selectedIndex = 0
|
||||
|
@ -64,7 +64,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject
|
|||
|
||||
fun getData(): ZigProjectConfigurationData {
|
||||
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) {
|
||||
|
@ -73,6 +73,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject
|
|||
cell(git)
|
||||
}
|
||||
}
|
||||
panels.filter { it.newProjectBeforeInitSelector }.forEach { it.attach(p) }
|
||||
group("Zig Project Template") {
|
||||
row {
|
||||
resizableRow()
|
||||
|
@ -81,7 +82,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable, ZigProject
|
|||
.align(AlignY.FILL)
|
||||
}
|
||||
}
|
||||
panels.forEach { it.attach(p) }
|
||||
panels.filter { !it.newProjectBeforeInitSelector }.forEach { it.attach(p) }
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
|
|
|
@ -22,9 +22,10 @@
|
|||
|
||||
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.ZigProjectTemplate
|
||||
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
|
||||
import com.falsepattern.zigbrains.shared.SubConfigurable
|
||||
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
||||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationType
|
||||
|
@ -42,7 +43,7 @@ import kotlinx.coroutines.launch
|
|||
@JvmRecord
|
||||
data class ZigProjectConfigurationData(
|
||||
val git: Boolean,
|
||||
val conf: List<ZigProjectConfigurationProvider.Settings>,
|
||||
val conf: List<SubConfigurable<Project>>,
|
||||
val selectedTemplate: ZigProjectTemplate
|
||||
) {
|
||||
@RequiresBackgroundThread
|
||||
|
@ -54,9 +55,7 @@ data class ZigProjectConfigurationData(
|
|||
|
||||
if (!reporter.indeterminateStep("Initializing project") {
|
||||
if (template is ZigInitTemplate) {
|
||||
val toolchain = conf
|
||||
.mapNotNull { it as? ZigProjectConfigurationProvider.ToolchainProvider }
|
||||
.firstNotNullOfOrNull { it.toolchain } ?: run {
|
||||
val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run {
|
||||
Notification(
|
||||
"zigbrains",
|
||||
"Tried to generate project with zig init, but zig toolchain is invalid",
|
||||
|
|
|
@ -31,9 +31,9 @@ import com.intellij.platform.ProjectGeneratorPeer
|
|||
import com.intellij.ui.dsl.builder.panel
|
||||
import javax.swing.JComponent
|
||||
|
||||
class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigProjectConfigurationData>, Disposable {
|
||||
private val newProjectPanel by lazy {
|
||||
ZigNewProjectPanel(handleGit).also { Disposer.register(this, it) }
|
||||
class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigProjectConfigurationData> {
|
||||
val newProjectPanel by lazy {
|
||||
ZigNewProjectPanel(handleGit)
|
||||
}
|
||||
val myComponent: JComponent by lazy {
|
||||
panel {
|
||||
|
@ -61,6 +61,7 @@ class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigP
|
|||
return false
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
fun dispose() {
|
||||
newProjectPanel.dispose()
|
||||
}
|
||||
}
|
|
@ -23,8 +23,8 @@
|
|||
package com.falsepattern.zigbrains.project.run
|
||||
|
||||
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
|
||||
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
|
||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.configurations.RunProfileState
|
||||
|
@ -61,7 +61,7 @@ abstract class ZigProgramRunner<ProfileState : ZigProfileState<*>>(protected val
|
|||
|
||||
val state = castProfileState(baseState) ?: return null
|
||||
|
||||
val toolchain = environment.project.zigProjectSettings.state.toolchain ?: run {
|
||||
val toolchain = ZigToolchainService.getInstance(environment.project).toolchain ?: run {
|
||||
Notification(
|
||||
"zigbrains",
|
||||
"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?
|
||||
|
||||
@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?
|
||||
}
|
|
@ -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.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.intellij.execution.configurations.RunProfile
|
||||
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.ui.RunContentDescriptor
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.progress.blockingContext
|
||||
|
||||
class ZigRegularRunner: ZigProgramRunner<ZigProfileState<*>>(DefaultRunExecutor.EXECUTOR_ID) {
|
||||
override suspend fun execute(state: ZigProfileState<*>, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? {
|
||||
val exec = state.execute(environment.executor, this)
|
||||
override suspend fun execute(state: ZigProfileState<*>, toolchain: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor? {
|
||||
val exec = blockingContext {
|
||||
state.execute(environment.executor, this)
|
||||
}
|
||||
return withEDTContext(ModalityState.any()) {
|
||||
val runContentBuilder = RunContentBuilder(exec, environment)
|
||||
runContentBuilder.showRunContent(null)
|
||||
|
|
|
@ -22,11 +22,14 @@
|
|||
|
||||
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
|
||||
|
||||
class ZigConfigurable(project: Project): MultiConfigurable(ZigProjectConfigurationProvider.createConfigurables(project)) {
|
||||
override fun getDisplayName(): String {
|
||||
return "Zig"
|
||||
class ZigConfigurable(override val context: Project) : SubConfigurable.Adapter<Project>() {
|
||||
override fun instantiate(): List<SubConfigurable<Project>> {
|
||||
return ZigProjectConfigurationProvider.createPanels(context)
|
||||
}
|
||||
|
||||
override fun getDisplayName() = ZigBrainsBundle.message("settings.project.display-name")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -22,42 +22,56 @@
|
|||
|
||||
package com.falsepattern.zigbrains.project.settings
|
||||
|
||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||
import com.falsepattern.zigbrains.shared.SubConfigurable
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.extensions.ExtensionPointName
|
||||
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 {
|
||||
fun handleMainConfigChanged(project: Project)
|
||||
fun createConfigurable(project: Project): SubConfigurable
|
||||
fun createNewProjectSettingsPanel(holder: SettingsPanelHolder): SettingsPanel?
|
||||
val priority: Int
|
||||
fun create(sharedState: IUserDataBridge): SubConfigurable<Project>?
|
||||
val index: Int
|
||||
companion object {
|
||||
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigProjectConfigurationProvider>("com.falsepattern.zigbrains.projectConfigProvider")
|
||||
fun mainConfigChanged(project: Project) {
|
||||
EXTENSION_POINT_NAME.extensionList.forEach { it.handleMainConfigChanged(project) }
|
||||
}
|
||||
fun createConfigurables(project: Project): List<SubConfigurable> {
|
||||
return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.map { it.createConfigurable(project) }
|
||||
}
|
||||
fun createNewProjectSettingsPanels(holder: SettingsPanelHolder): List<SettingsPanel> {
|
||||
return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.mapNotNull { it.createNewProjectSettingsPanel(holder) }
|
||||
val PROJECT_KEY: Key<Project> = Key.create("Project")
|
||||
fun createPanels(project: Project?): List<SubConfigurable<Project>> {
|
||||
val sharedState = UserDataBridge()
|
||||
sharedState.putUserData(PROJECT_KEY, project)
|
||||
return EXTENSION_POINT_NAME.extensionList.sortedBy { it.index }.mapNotNull { it.create(sharedState) }
|
||||
}
|
||||
}
|
||||
interface SettingsPanel: Disposable {
|
||||
val data: Settings
|
||||
fun attach(p: Panel)
|
||||
fun direnvChanged(state: Boolean)
|
||||
|
||||
interface IUserDataBridge: UserDataHolder {
|
||||
fun addUserDataChangeListener(listener: UserDataListener)
|
||||
fun removeUserDataChangeListener(listener: UserDataListener)
|
||||
}
|
||||
interface SettingsPanelHolder {
|
||||
val panels: List<SettingsPanel>
|
||||
|
||||
interface UserDataListener {
|
||||
fun onUserDataChanged(key: Key<*>)
|
||||
}
|
||||
interface Settings {
|
||||
fun apply(project: Project)
|
||||
|
||||
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 ToolchainProvider {
|
||||
val toolchain: AbstractZigToolchain?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>()
|
|
@ -20,7 +20,7 @@
|
|||
* 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.roots.AdditionalLibraryRootsProvider
|
|
@ -20,41 +20,43 @@
|
|||
* 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.project.settings.ZigProjectSettings
|
||||
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
|
||||
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
|
||||
import com.intellij.navigation.ItemPresentation
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectDir
|
||||
import com.intellij.openapi.roots.SyntheticLibrary
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.vfs.refreshAndFindVirtualDirectory
|
||||
import com.intellij.platform.backend.workspace.WorkspaceModel
|
||||
import com.intellij.platform.backend.workspace.toVirtualFileUrl
|
||||
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 kotlinx.coroutines.runBlocking
|
||||
import java.util.*
|
||||
import javax.swing.Icon
|
||||
|
||||
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 {
|
||||
runBlocking {getRoot(state, project)}?.let { setOf(it) } ?: emptySet()
|
||||
runBlocking {getRoot(toolchain, project)}?.let { setOf(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
private val name by lazy {
|
||||
getName(state, project)
|
||||
getName(toolchain, project)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is ZigSyntheticLibrary)
|
||||
return false
|
||||
|
||||
return state == other.state
|
||||
return toolchain == other.toolchain
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
|
@ -75,15 +77,23 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen
|
|||
|
||||
companion object {
|
||||
private const val ZIG_LIBRARY_ID = "Zig SDK"
|
||||
private const val ZIG_MODULE_ID = "Zig"
|
||||
suspend fun reload(project: Project, state: ZigProjectSettings) {
|
||||
private const val ZIG_MODULE_ID = "ZigBrains"
|
||||
suspend fun reload(project: Project, toolchain: ZigToolchain?) {
|
||||
val moduleId = ModuleId(ZIG_MODULE_ID)
|
||||
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 libraryTableId = LibraryTableId.ProjectLibraryTableId
|
||||
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 ->
|
||||
builder.resolve(moduleId)?.let { moduleEntity ->
|
||||
builder.removeEntity(moduleEntity)
|
||||
|
@ -118,37 +128,39 @@ class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresen
|
|||
}
|
||||
|
||||
private fun getName(
|
||||
state: ZigProjectSettings,
|
||||
toolchain: ZigToolchain?,
|
||||
project: Project
|
||||
): String {
|
||||
val tc = state.toolchain ?: return "Zig"
|
||||
val version = runBlocking { tc.zig.getEnv(project) }.mapCatching { it.version }.getOrElse { return "Zig" }
|
||||
return "Zig $version"
|
||||
val tc = toolchain ?: return "Zig"
|
||||
toolchain.name?.let { return it }
|
||||
runBlocking { tc.zig.getEnv(project) }
|
||||
.mapCatching { it.version }
|
||||
.getOrNull()
|
||||
?.let { return "Zig $it" }
|
||||
return "Zig"
|
||||
}
|
||||
|
||||
suspend fun getRoot(
|
||||
state: ZigProjectSettings,
|
||||
toolchain: ZigToolchain?,
|
||||
project: Project
|
||||
): VirtualFile? {
|
||||
val toolchain = state.toolchain
|
||||
if (state.overrideStdPath) run {
|
||||
val ePathStr = state.explicitPathToStd ?: return@run
|
||||
val ePath = ePathStr.toNioPathOrNull() ?: return@run
|
||||
//TODO universal
|
||||
if (toolchain !is LocalZigToolchain) {
|
||||
return null
|
||||
}
|
||||
if (toolchain.std != null) run {
|
||||
val ePath = toolchain.std
|
||||
if (ePath.isAbsolute) {
|
||||
val roots = ePath.refreshAndFindVirtualDirectory() ?: return@run
|
||||
return roots
|
||||
} else if (toolchain != null) {
|
||||
val stdPath = toolchain.location.resolve(ePath)
|
||||
if (stdPath.isAbsolute) {
|
||||
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run
|
||||
return roots
|
||||
}
|
||||
}
|
||||
val stdPath = toolchain.location.resolve(ePath)
|
||||
if (stdPath.isAbsolute) {
|
||||
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run
|
||||
return roots
|
||||
}
|
||||
}
|
||||
if (toolchain != null) {
|
||||
val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null
|
||||
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null
|
||||
return roots
|
||||
}
|
||||
return null
|
||||
val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null
|
||||
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null
|
||||
return roots
|
||||
}
|
|
@ -22,8 +22,8 @@
|
|||
|
||||
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.toolchain.ZigToolchainService
|
||||
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
|
||||
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
||||
import com.intellij.openapi.Disposable
|
||||
|
@ -76,7 +76,7 @@ class ZigStepDiscoveryService(private val project: Project) {
|
|||
|
||||
private tailrec suspend fun doReload() {
|
||||
preReload()
|
||||
val toolchain = project.zigProjectSettings.state.toolchain ?: run {
|
||||
val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run {
|
||||
errorReload(ErrorType.MissingToolchain)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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>()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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?) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -20,27 +20,30 @@
|
|||
* 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.project.settings.zigProjectSettings
|
||||
import com.falsepattern.zigbrains.direnv.DirenvService
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.configurations.GeneralCommandLine
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectDir
|
||||
import com.intellij.openapi.util.KeyWithDefaultValue
|
||||
import com.intellij.openapi.util.Key
|
||||
import com.intellij.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import com.intellij.openapi.vfs.toNioPathOrNull
|
||||
import com.intellij.util.keyFMap.KeyFMap
|
||||
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? {
|
||||
return project?.guessProjectDir()?.toNioPathOrNull()
|
||||
}
|
||||
|
||||
override suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project?): GeneralCommandLine {
|
||||
if (project != null && (commandLine.getUserData(DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) {
|
||||
commandLine.withEnvironment(DirenvCmd.importDirenv(project).env)
|
||||
if (project != null && DirenvService.getStateFor(commandLine, project).isEnabled(project)) {
|
||||
commandLine.withEnvironment(DirenvService.getInstance(project).import().env)
|
||||
}
|
||||
return commandLine
|
||||
}
|
||||
|
@ -50,11 +53,17 @@ class LocalZigToolchain(val location: Path): AbstractZigToolchain() {
|
|||
return location.resolve(exeName)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val DIRENV_KEY = KeyWithDefaultValue.create<Boolean>("ZIG_LOCAL_DIRENV")
|
||||
override fun withExtraData(map: Map<String, String>): ZigToolchain {
|
||||
return this.copy(extraData = map)
|
||||
}
|
||||
|
||||
override fun withName(newName: String?): LocalZigToolchain {
|
||||
return this.copy(name = newName)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Throws(ExecutionException::class)
|
||||
fun ensureLocal(toolchain: AbstractZigToolchain): LocalZigToolchain {
|
||||
fun ensureLocal(toolchain: ZigToolchain): LocalZigToolchain {
|
||||
if (toolchain is LocalZigToolchain) {
|
||||
return toolchain
|
||||
} else {
|
||||
|
@ -62,5 +71,20 @@ class LocalZigToolchain(val location: Path): AbstractZigToolchain() {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,28 +20,21 @@
|
|||
* 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.shared.SubConfigurable
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ProjectManager
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchainConfigurable
|
||||
import java.util.UUID
|
||||
|
||||
class ZLSProjectConfigurationProvider: ZigProjectConfigurationProvider {
|
||||
override fun handleMainConfigChanged(project: Project) {
|
||||
startLSP(project, true)
|
||||
class LocalZigToolchainConfigurable(
|
||||
uuid: UUID,
|
||||
toolchain: LocalZigToolchain,
|
||||
data: ZigProjectConfigurationProvider.IUserDataBridge?,
|
||||
modal: Boolean
|
||||
): ZigToolchainConfigurable<LocalZigToolchain>(uuid, toolchain, data, modal) {
|
||||
override fun createPanel() = LocalZigToolchainPanel()
|
||||
|
||||
override fun setDisplayName(name: String?) {
|
||||
toolchain = toolchain.copy(name = name)
|
||||
}
|
||||
|
||||
override fun createConfigurable(project: Project): SubConfigurable {
|
||||
return ZLSSettingsConfigurable(project)
|
||||
}
|
||||
|
||||
override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel {
|
||||
return ZLSSettingsPanel(ProjectManager.getInstance().defaultProject)
|
||||
}
|
||||
|
||||
override val priority: Int
|
||||
get() = 1000
|
||||
}
|
|
@ -20,24 +20,16 @@
|
|||
* 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.direnv.DirenvCmd
|
||||
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.project.toolchain.ui.ImmutableNamedElementPanelBase
|
||||
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.project.Project
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
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.JBColor
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
|
@ -52,12 +44,9 @@ import kotlinx.coroutines.launch
|
|||
import javax.swing.event.DocumentEvent
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvider.SettingsPanelHolder, private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel {
|
||||
private val direnv = JBCheckBox(ZigBrainsBundle.message("settings.project.label.direnv")).apply { addActionListener {
|
||||
dispatchDirenvUpdate()
|
||||
} }
|
||||
class LocalZigToolchainPanel() : ImmutableNamedElementPanelBase<LocalZigToolchain>() {
|
||||
private val pathToToolchain = textFieldWithBrowseButton(
|
||||
project,
|
||||
null,
|
||||
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
|
||||
).also {
|
||||
it.textField.document.addDocumentListener(object : DocumentAdapter() {
|
||||
|
@ -68,7 +57,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide
|
|||
Disposer.register(this, it)
|
||||
}
|
||||
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 {
|
||||
if (isSelected) {
|
||||
pathToStd.isEnabled = true
|
||||
|
@ -79,92 +68,57 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide
|
|||
}
|
||||
}
|
||||
private val pathToStd = textFieldWithBrowseButton(
|
||||
project,
|
||||
null,
|
||||
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-std"))
|
||||
).also { Disposer.register(this, it) }
|
||||
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) {
|
||||
data = project.zigProjectSettings.state
|
||||
if (project.isDefault) {
|
||||
row(ZigBrainsBundle.message("settings.project.label.toolchain")) {
|
||||
cell(pathToToolchain).resizableColumn().align(AlignX.FILL)
|
||||
}
|
||||
row(ZigBrainsBundle.message("settings.project.label.toolchain-version")) {
|
||||
cell(toolchainVersion)
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
super.attach(p)
|
||||
row(ZigBrainsBundle.message("settings.toolchain.local.path.label")) {
|
||||
cell(pathToToolchain).resizableColumn().align(AlignX.FILL)
|
||||
}
|
||||
row(ZigBrainsBundle.message("settings.toolchain.local.version.label")) {
|
||||
cell(toolchainVersion)
|
||||
}
|
||||
row(ZigBrainsBundle.message("settings.toolchain.local.std.label")) {
|
||||
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() {
|
||||
debounce?.cancel("New debounce")
|
||||
debounce = project.zigCoroutineScope.launch {
|
||||
debounce = zigCoroutineScope.launch {
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
|
@ -183,7 +137,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide
|
|||
}
|
||||
val toolchain = LocalZigToolchain(pathToToolchain)
|
||||
val zig = toolchain.zig
|
||||
val env = zig.getEnv(project).getOrElse { throwable ->
|
||||
val env = zig.getEnv(null).getOrElse { throwable ->
|
||||
throwable.printStackTrace()
|
||||
withEDTContext(ModalityState.any()) {
|
||||
toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}"
|
||||
|
@ -194,7 +148,7 @@ class ZigProjectSettingsPanel(private val holder: ZigProjectConfigurationProvide
|
|||
return
|
||||
}
|
||||
val version = env.version
|
||||
val stdPath = env.stdPath(toolchain, project)
|
||||
val stdPath = env.stdPath(toolchain, null)
|
||||
|
||||
withEDTContext(ModalityState.any()) {
|
||||
toolchainVersion.text = version
|
|
@ -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
|
||||
}
|
|
@ -22,14 +22,14 @@
|
|||
|
||||
package com.falsepattern.zigbrains.project.toolchain.tools
|
||||
|
||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
||||
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSerializable
|
||||
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
|
||||
import com.intellij.openapi.project.Project
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.nio.file.Path
|
||||
|
||||
class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) {
|
||||
class ZigCompilerTool(toolchain: ZigToolchain) : ZigTool(toolchain) {
|
||||
override val toolName: String
|
||||
get() = "zig"
|
||||
|
||||
|
|
|
@ -22,15 +22,16 @@
|
|||
|
||||
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.createCommandLineSafe
|
||||
import com.intellij.execution.configurations.GeneralCommandLine
|
||||
import com.intellij.execution.process.ProcessOutput
|
||||
import com.intellij.openapi.project.Project
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun fileValid(): Boolean {
|
||||
val exe = toolchain.pathToExecutable(toolName)
|
||||
return exe.isRegularFile()
|
||||
}
|
||||
|
||||
private suspend fun createBaseCommandLine(
|
||||
workingDirectory: Path?,
|
||||
vararg parameters: String
|
||||
|
|
|
@ -19,15 +19,16 @@
|
|||
* 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
|
||||
|
||||
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.util.io.toNioPathOrNull
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.nio.file.Path
|
||||
|
||||
|
||||
@JvmRecord
|
||||
@Serializable
|
||||
data class ZigToolchainEnvironmentSerializable(
|
||||
|
@ -49,4 +50,4 @@ data class ZigToolchainEnvironmentSerializable(
|
|||
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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?)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -22,16 +22,73 @@
|
|||
|
||||
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.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 java.util.ArrayList
|
||||
import javax.swing.JComponent
|
||||
|
||||
interface SubConfigurable: Disposable {
|
||||
fun createComponent(holder: SettingsPanelHolder, panel: Panel): ZigProjectConfigurationProvider.SettingsPanel
|
||||
fun isModified(): Boolean
|
||||
@Throws(ConfigurationException::class)
|
||||
fun apply()
|
||||
fun reset()
|
||||
interface SubConfigurable<T>: Disposable {
|
||||
fun attach(panel: Panel)
|
||||
fun isModified(context: T): Boolean
|
||||
fun apply(context: T)
|
||||
fun reset(context: T?)
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,7 +28,6 @@ import com.falsepattern.zigbrains.shared.ipc.IPCUtil
|
|||
import com.falsepattern.zigbrains.shared.ipc.ipc
|
||||
import com.intellij.execution.ExecutionException
|
||||
import com.intellij.execution.configurations.GeneralCommandLine
|
||||
import com.intellij.execution.process.ProcessHandler
|
||||
import com.intellij.execution.process.ProcessOutput
|
||||
import com.intellij.execution.process.ProcessTerminatedListener
|
||||
import com.intellij.openapi.options.ConfigurationException
|
||||
|
|
|
@ -30,6 +30,8 @@ import com.intellij.platform.ide.progress.TaskCancellation
|
|||
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
|
||||
import com.intellij.util.application
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
@ -50,9 +56,19 @@ suspend inline fun <T> withCurrentEDTModalityContext(noinline block: suspend Cor
|
|||
}
|
||||
|
||||
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 {
|
||||
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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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()
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
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.openapi.util.SystemInfo
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
|
@ -56,7 +56,7 @@ object IPCUtil {
|
|||
if (SystemInfo.isWindows) {
|
||||
return null
|
||||
}
|
||||
val mkfifo = emptyEnv
|
||||
val mkfifo = Env.empty
|
||||
.findAllExecutablesOnPATH("mkfifo")
|
||||
.map { it.pathString }
|
||||
.map(::MKFifo)
|
||||
|
@ -67,7 +67,7 @@ object IPCUtil {
|
|||
true
|
||||
} ?: return null
|
||||
|
||||
val selectedBash = emptyEnv
|
||||
val selectedBash = Env.empty
|
||||
.findAllExecutablesOnPATH("bash")
|
||||
.map { it.pathString }
|
||||
.filter {
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
@ -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)
|
|
@ -142,7 +142,15 @@
|
|||
parentId="language"
|
||||
instance="com.falsepattern.zigbrains.project.settings.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
|
||||
|
@ -158,7 +166,7 @@
|
|||
/>
|
||||
|
||||
<additionalLibraryRootsProvider
|
||||
implementation="com.falsepattern.zigbrains.project.toolchain.stdlib.ZigLibraryRootProvider"
|
||||
implementation="com.falsepattern.zigbrains.project.stdlib.ZigLibraryRootProvider"
|
||||
/>
|
||||
|
||||
<!--suppress PluginXmlValidity -->
|
||||
|
@ -177,10 +185,13 @@
|
|||
|
||||
<extensions defaultExtensionNs="com.falsepattern.zigbrains">
|
||||
<toolchainProvider
|
||||
implementation="com.falsepattern.zigbrains.project.toolchain.LocalZigToolchainProvider"
|
||||
implementation="com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchainProvider"
|
||||
/>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -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.timeout=zig build -l timed out after {0} seconds.
|
||||
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
21
licenses/ZLS.LICENSE
Normal 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.
|
|
@ -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)
|
||||
}
|
|
@ -20,24 +20,22 @@
|
|||
* 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.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.NotificationType
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.UserDataHolderBase
|
||||
import com.intellij.openapi.util.io.toNioPathOrNull
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
|
||||
class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider {
|
||||
override suspend fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig {
|
||||
val svc = project.zigProjectSettings
|
||||
var state = svc.state
|
||||
val toolchain = state.toolchain ?: ZigToolchainProvider.suggestToolchain(project, UserDataHolderBase()) ?: return previous
|
||||
val svc = ZigToolchainService.getInstance(project)
|
||||
val toolchain = svc.toolchain ?: return previous
|
||||
|
||||
val env = toolchain.zig.getEnv(project).getOrElse { throwable ->
|
||||
throwable.printStackTrace()
|
||||
|
@ -65,16 +63,10 @@ class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider {
|
|||
).notify(project)
|
||||
return previous
|
||||
}
|
||||
var lib = if (state.overrideStdPath && state.explicitPathToStd != null) {
|
||||
state.explicitPathToStd?.toNioPathOrNull() ?: run {
|
||||
Notification(
|
||||
"zigbrains-lsp",
|
||||
"Invalid zig standard library path override: ${state.explicitPathToStd}",
|
||||
NotificationType.ERROR
|
||||
).notify(project)
|
||||
null
|
||||
}
|
||||
} else null
|
||||
var lib = if (toolchain is LocalZigToolchain)
|
||||
toolchain.std
|
||||
else
|
||||
null
|
||||
|
||||
if (lib == null) {
|
||||
lib = env.libDirectory.toNioPathOrNull() ?: run {
|
|
@ -22,36 +22,26 @@
|
|||
|
||||
package com.falsepattern.zigbrains.lsp
|
||||
|
||||
import com.falsepattern.zigbrains.direnv.DirenvCmd
|
||||
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.lsp.zls.zls
|
||||
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.startup.ProjectActivity
|
||||
import com.intellij.ui.EditorNotifications
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
class ZLSStartup: ProjectActivity {
|
||||
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 {
|
||||
var currentState = project.zlsRunningAsync()
|
||||
var currentState = project.zlsRunning()
|
||||
var currentZLS = project.zls
|
||||
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) {
|
||||
EditorNotifications.getInstance(project).updateAllNotifications()
|
||||
}
|
||||
|
|
|
@ -22,11 +22,9 @@
|
|||
|
||||
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.settings.zlsSettings
|
||||
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
|
||||
import com.falsepattern.zigbrains.lsp.zls.zls
|
||||
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
|
||||
import com.intellij.execution.configurations.GeneralCommandLine
|
||||
import com.intellij.notification.Notification
|
||||
import com.intellij.notification.NotificationType
|
||||
|
@ -55,30 +53,8 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje
|
|||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend fun getCommand(project: Project): List<String>? {
|
||||
val svc = project.zlsSettings
|
||||
val state = svc.state
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
val zls = project.zls ?: return null
|
||||
val zlsPath: Path = zls.path
|
||||
if (!zlsPath.toFile().exists()) {
|
||||
Notification(
|
||||
"zigbrains-lsp",
|
||||
|
@ -95,7 +71,7 @@ class ZLSStreamConnectionProvider private constructor(private val project: Proje
|
|||
).notify(project)
|
||||
return null
|
||||
}
|
||||
val configPath: Path? = state.zlsConfigPath.let { configPath ->
|
||||
val configPath: Path? = "".let { configPath ->
|
||||
if (configPath.isNotBlank()) {
|
||||
configPath.toNioPathOrNull()?.let { nioPath ->
|
||||
if (!nioPath.toFile().exists()) {
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
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.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
|
@ -68,39 +68,29 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS
|
|||
}
|
||||
features.inlayHintFeature = object: LSPInlayHintFeature() {
|
||||
override fun isEnabled(file: PsiFile): Boolean {
|
||||
return features.project.zlsSettings.state.inlayHints
|
||||
return project.zls?.settings?.inlayHints == true
|
||||
}
|
||||
}
|
||||
return features
|
||||
}
|
||||
|
||||
override fun isEnabled(project: Project) = project.zlsEnabledSync()
|
||||
override fun isEnabled(project: Project) = project.zlsEnabled()
|
||||
|
||||
override fun setEnabled(enabled: Boolean, project: Project) {
|
||||
project.zlsEnabled(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun Project.zlsEnabledAsync(): Boolean {
|
||||
return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateAsync()
|
||||
}
|
||||
|
||||
fun Project.zlsEnabledSync(): Boolean {
|
||||
return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateSync()
|
||||
fun Project.zlsEnabled(): Boolean {
|
||||
return (getUserData(ENABLED_KEY) != false) && zls?.isValid() == true
|
||||
}
|
||||
|
||||
fun Project.zlsEnabled(value: Boolean) {
|
||||
putUserData(ENABLED_KEY, value)
|
||||
}
|
||||
|
||||
suspend fun Project.zlsRunningAsync(): Boolean {
|
||||
if (!zlsEnabledAsync())
|
||||
return false
|
||||
return lsm.isRunning
|
||||
}
|
||||
|
||||
fun Project.zlsRunningSync(): Boolean {
|
||||
if (!zlsEnabledSync())
|
||||
fun Project.zlsRunning(): Boolean {
|
||||
if (!zlsEnabled())
|
||||
return false
|
||||
return lsm.isRunning
|
||||
}
|
||||
|
@ -135,7 +125,7 @@ private suspend fun doStart(project: Project, restart: Boolean) {
|
|||
project.lsm.stop("ZigBrains")
|
||||
delay(250)
|
||||
}
|
||||
if (project.zlsSettings.validateAsync()) {
|
||||
if (project.zls?.isValid() == true) {
|
||||
delay(250)
|
||||
project.lsm.start("ZigBrains")
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
package com.falsepattern.zigbrains.lsp.notification
|
||||
|
||||
import com.falsepattern.zigbrains.lsp.ZLSBundle
|
||||
import com.falsepattern.zigbrains.lsp.settings.zlsSettings
|
||||
import com.falsepattern.zigbrains.lsp.zlsRunningAsync
|
||||
import com.falsepattern.zigbrains.lsp.zls.zls
|
||||
import com.falsepattern.zigbrains.lsp.zlsRunning
|
||||
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
||||
import com.falsepattern.zigbrains.zig.ZigFileType
|
||||
import com.falsepattern.zigbrains.zon.ZonFileType
|
||||
|
@ -49,10 +49,10 @@ class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware {
|
|||
else -> return null
|
||||
}
|
||||
val task = project.zigCoroutineScope.async {
|
||||
if (project.zlsRunningAsync()) {
|
||||
if (project.zlsRunning()) {
|
||||
return@async null
|
||||
} else {
|
||||
return@async project.zlsSettings.validateAsync()
|
||||
return@async project.zls?.isValid() == true
|
||||
}
|
||||
}
|
||||
return Function { editor ->
|
||||
|
|
|
@ -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>()
|
|
@ -25,35 +25,31 @@ package com.falsepattern.zigbrains.lsp.settings
|
|||
import com.falsepattern.zigbrains.lsp.config.SemanticTokens
|
||||
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.util.xmlb.annotations.Attribute
|
||||
import org.jetbrains.annotations.NonNls
|
||||
|
||||
@Suppress("PropertyName")
|
||||
data class ZLSSettings(
|
||||
var zlsPath: @NonNls String = "",
|
||||
var zlsConfigPath: @NonNls String = "",
|
||||
val inlayHints: Boolean = true,
|
||||
val enable_snippets: Boolean = true,
|
||||
val enable_argument_placeholders: Boolean = true,
|
||||
val completion_label_details: Boolean = true,
|
||||
val enable_build_on_save: Boolean = false,
|
||||
val build_on_save_args: String = "",
|
||||
val semantic_tokens: SemanticTokens = SemanticTokens.full,
|
||||
val inlay_hints_show_variable_type_hints: Boolean = true,
|
||||
val inlay_hints_show_struct_literal_field_type: Boolean = true,
|
||||
val inlay_hints_show_parameter_name: Boolean = true,
|
||||
val inlay_hints_show_builtin: Boolean = true,
|
||||
val inlay_hints_exclude_single_argument: Boolean = true,
|
||||
val inlay_hints_hide_redundant_param_names: Boolean = false,
|
||||
val inlay_hints_hide_redundant_param_names_last_token: Boolean = false,
|
||||
val warn_style: Boolean = false,
|
||||
val highlight_global_var_declarations: Boolean = false,
|
||||
val skip_std_references: Boolean = false,
|
||||
val prefer_ast_check_as_child_process: Boolean = true,
|
||||
val builtin_path: String? = null,
|
||||
val build_runner_path: @NonNls String? = null,
|
||||
val global_cache_path: @NonNls String? = null,
|
||||
): ZigProjectConfigurationProvider.Settings {
|
||||
override fun apply(project: Project) {
|
||||
project.zlsSettings.loadState(this)
|
||||
}
|
||||
}
|
||||
@JvmField @Attribute val zlsConfigPath: @NonNls String = "",
|
||||
@JvmField @Attribute val inlayHints: Boolean = true,
|
||||
@JvmField @Attribute val enable_snippets: Boolean = true,
|
||||
@JvmField @Attribute val enable_argument_placeholders: Boolean = true,
|
||||
@JvmField @Attribute val completion_label_details: Boolean = true,
|
||||
@JvmField @Attribute val enable_build_on_save: Boolean = false,
|
||||
@JvmField @Attribute val build_on_save_args: String = "",
|
||||
@JvmField @Attribute val semantic_tokens: SemanticTokens = SemanticTokens.full,
|
||||
@JvmField @Attribute val inlay_hints_show_variable_type_hints: Boolean = true,
|
||||
@JvmField @Attribute val inlay_hints_show_struct_literal_field_type: Boolean = true,
|
||||
@JvmField @Attribute val inlay_hints_show_parameter_name: Boolean = true,
|
||||
@JvmField @Attribute val inlay_hints_show_builtin: Boolean = true,
|
||||
@JvmField @Attribute val inlay_hints_exclude_single_argument: Boolean = true,
|
||||
@JvmField @Attribute val inlay_hints_hide_redundant_param_names: Boolean = false,
|
||||
@JvmField @Attribute val inlay_hints_hide_redundant_param_names_last_token: Boolean = false,
|
||||
@JvmField @Attribute val warn_style: Boolean = false,
|
||||
@JvmField @Attribute val highlight_global_var_declarations: Boolean = false,
|
||||
@JvmField @Attribute val skip_std_references: Boolean = false,
|
||||
@JvmField @Attribute val prefer_ast_check_as_child_process: Boolean = true,
|
||||
@JvmField @Attribute val builtin_path: String? = null,
|
||||
@JvmField @Attribute val build_runner_path: @NonNls String? = null,
|
||||
@JvmField @Attribute val global_cache_path: @NonNls String? = null,
|
||||
)
|
|
@ -24,12 +24,13 @@ package com.falsepattern.zigbrains.lsp.settings
|
|||
|
||||
import com.falsepattern.zigbrains.lsp.config.ZLSConfig
|
||||
import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider
|
||||
import com.falsepattern.zigbrains.lsp.zls.zls
|
||||
import com.falsepattern.zigbrains.shared.cli.translateCommandline
|
||||
import com.intellij.openapi.project.Project
|
||||
|
||||
class ZLSSettingsConfigProvider: ZLSConfigProvider {
|
||||
override fun getEnvironment(project: Project, previous: ZLSConfig): ZLSConfig {
|
||||
val state = project.zlsSettings.state
|
||||
val state = project.zls?.settings ?: return previous
|
||||
return previous.copy(
|
||||
enable_snippets = state.enable_snippets,
|
||||
enable_argument_placeholders = state.enable_argument_placeholders,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -22,74 +22,27 @@
|
|||
|
||||
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.config.SemanticTokens
|
||||
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
|
||||
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.falsepattern.zigbrains.project.toolchain.ui.ImmutableElementPanel
|
||||
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.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.JBTextArea
|
||||
import com.intellij.ui.components.fields.ExtendableTextField
|
||||
import com.intellij.ui.components.textFieldWithBrowseButton
|
||||
import com.intellij.ui.dsl.builder.AlignX
|
||||
import com.intellij.ui.dsl.builder.Panel
|
||||
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 javax.swing.event.DocumentEvent
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
@Suppress("PrivatePropertyName")
|
||||
class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationProvider.SettingsPanel {
|
||||
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)
|
||||
}
|
||||
class ZLSSettingsPanel() : ImmutableElementPanel<ZLSSettings> {
|
||||
private val zlsConfigPath = textFieldWithBrowseButton(
|
||||
project,
|
||||
null,
|
||||
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
|
||||
.withTitle(ZLSBundle.message("settings.zls-config-path.browse.title"))
|
||||
).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 enable_snippets = JBCheckBox()
|
||||
private val enable_argument_placeholders = JBCheckBox()
|
||||
|
@ -112,121 +65,113 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
|
|||
private val build_runner_path = ExtendableTextField()
|
||||
private val global_cache_path = ExtendableTextField()
|
||||
|
||||
override fun attach(p: Panel) = 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(
|
||||
"settings.zls-config-path.label",
|
||||
"settings.zls-config-path.tooltip"
|
||||
) { cell(zlsConfigPath).align(AlignX.FILL) }
|
||||
fancyRow(
|
||||
"settings.enable_snippets.label",
|
||||
"settings.enable_snippets.tooltip"
|
||||
) { cell(enable_snippets) }
|
||||
fancyRow(
|
||||
"settings.enable_argument_placeholders.label",
|
||||
"settings.enable_argument_placeholders.tooltip"
|
||||
) { cell(enable_argument_placeholders) }
|
||||
fancyRow(
|
||||
"settings.completion_label_details.label",
|
||||
"settings.completion_label_details.tooltip"
|
||||
) { cell(completion_label_details) }
|
||||
fancyRow(
|
||||
"settings.enable_build_on_save.label",
|
||||
"settings.enable_build_on_save.tooltip"
|
||||
) { cell(enable_build_on_save) }
|
||||
fancyRow(
|
||||
"settings.build_on_save_args.label",
|
||||
"settings.build_on_save_args.tooltip"
|
||||
) { cell(build_on_save_args).resizableColumn().align(AlignX.FILL) }
|
||||
fancyRow(
|
||||
"settings.semantic_tokens.label",
|
||||
"settings.semantic_tokens.tooltip"
|
||||
) { cell(semantic_tokens) }
|
||||
group(ZLSBundle.message("settings.inlay-hints-group.label")) {
|
||||
fancyRow(
|
||||
"settings.inlay-hints-enable.label",
|
||||
"settings.inlay-hints-enable.tooltip"
|
||||
) { cell(inlayHints) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_show_variable_type_hints.label",
|
||||
"settings.inlay_hints_show_variable_type_hints.tooltip"
|
||||
) { cell(inlay_hints_show_variable_type_hints) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_show_struct_literal_field_type.label",
|
||||
"settings.inlay_hints_show_struct_literal_field_type.tooltip"
|
||||
) { cell(inlay_hints_show_struct_literal_field_type) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_show_parameter_name.label",
|
||||
"settings.inlay_hints_show_parameter_name.tooltip"
|
||||
) { cell(inlay_hints_show_parameter_name) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_show_builtin.label",
|
||||
"settings.inlay_hints_show_builtin.tooltip"
|
||||
) { cell(inlay_hints_show_builtin) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_exclude_single_argument.label",
|
||||
"settings.inlay_hints_exclude_single_argument.tooltip"
|
||||
) { cell(inlay_hints_exclude_single_argument) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_hide_redundant_param_names.label",
|
||||
"settings.inlay_hints_hide_redundant_param_names.tooltip"
|
||||
) { cell(inlay_hints_hide_redundant_param_names) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_hide_redundant_param_names_last_token.label",
|
||||
"settings.inlay_hints_hide_redundant_param_names_last_token.tooltip"
|
||||
) { cell(inlay_hints_hide_redundant_param_names_last_token) }
|
||||
}
|
||||
fancyRow(
|
||||
"settings.warn_style.label",
|
||||
"settings.warn_style.tooltip"
|
||||
) { cell(warn_style) }
|
||||
fancyRow(
|
||||
"settings.highlight_global_var_declarations.label",
|
||||
"settings.highlight_global_var_declarations.tooltip"
|
||||
) { cell(highlight_global_var_declarations) }
|
||||
fancyRow(
|
||||
"settings.skip_std_references.label",
|
||||
"settings.skip_std_references.tooltip"
|
||||
) { cell(skip_std_references) }
|
||||
fancyRow(
|
||||
"settings.prefer_ast_check_as_child_process.label",
|
||||
"settings.prefer_ast_check_as_child_process.tooltip"
|
||||
) { cell(prefer_ast_check_as_child_process) }
|
||||
fancyRow(
|
||||
"settings.builtin_path.label",
|
||||
"settings.builtin_path.tooltip"
|
||||
) { cell(builtin_path).resizableColumn().align(AlignX.FILL) }
|
||||
fancyRow(
|
||||
"settings.build_runner_path.label",
|
||||
"settings.build_runner_path.tooltip"
|
||||
) { cell(build_runner_path).resizableColumn().align(AlignX.FILL) }
|
||||
fancyRow(
|
||||
"settings.global_cache_path.label",
|
||||
"settings.global_cache_path.tooltip"
|
||||
) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) }
|
||||
}
|
||||
override fun attach(p: Panel): Unit = with(p) {
|
||||
fancyRow(
|
||||
"settings.zls-config-path.label",
|
||||
"settings.zls-config-path.tooltip"
|
||||
) { cell(zlsConfigPath).align(AlignX.FILL) }
|
||||
fancyRow(
|
||||
"settings.enable_snippets.label",
|
||||
"settings.enable_snippets.tooltip"
|
||||
) { cell(enable_snippets) }
|
||||
fancyRow(
|
||||
"settings.enable_argument_placeholders.label",
|
||||
"settings.enable_argument_placeholders.tooltip"
|
||||
) { cell(enable_argument_placeholders) }
|
||||
fancyRow(
|
||||
"settings.completion_label_details.label",
|
||||
"settings.completion_label_details.tooltip"
|
||||
) { cell(completion_label_details) }
|
||||
fancyRow(
|
||||
"settings.enable_build_on_save.label",
|
||||
"settings.enable_build_on_save.tooltip"
|
||||
) { cell(enable_build_on_save) }
|
||||
fancyRow(
|
||||
"settings.build_on_save_args.label",
|
||||
"settings.build_on_save_args.tooltip"
|
||||
) { cell(build_on_save_args).resizableColumn().align(AlignX.FILL) }
|
||||
fancyRow(
|
||||
"settings.semantic_tokens.label",
|
||||
"settings.semantic_tokens.tooltip"
|
||||
) { cell(semantic_tokens) }
|
||||
collapsibleGroup(ZLSBundle.message("settings.inlay-hints-group.label"), indent = false) {
|
||||
fancyRow(
|
||||
"settings.inlay-hints-enable.label",
|
||||
"settings.inlay-hints-enable.tooltip"
|
||||
) { cell(inlayHints) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_show_variable_type_hints.label",
|
||||
"settings.inlay_hints_show_variable_type_hints.tooltip"
|
||||
) { cell(inlay_hints_show_variable_type_hints) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_show_struct_literal_field_type.label",
|
||||
"settings.inlay_hints_show_struct_literal_field_type.tooltip"
|
||||
) { cell(inlay_hints_show_struct_literal_field_type) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_show_parameter_name.label",
|
||||
"settings.inlay_hints_show_parameter_name.tooltip"
|
||||
) { cell(inlay_hints_show_parameter_name) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_show_builtin.label",
|
||||
"settings.inlay_hints_show_builtin.tooltip"
|
||||
) { cell(inlay_hints_show_builtin) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_exclude_single_argument.label",
|
||||
"settings.inlay_hints_exclude_single_argument.tooltip"
|
||||
) { cell(inlay_hints_exclude_single_argument) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_hide_redundant_param_names.label",
|
||||
"settings.inlay_hints_hide_redundant_param_names.tooltip"
|
||||
) { cell(inlay_hints_hide_redundant_param_names) }
|
||||
fancyRow(
|
||||
"settings.inlay_hints_hide_redundant_param_names_last_token.label",
|
||||
"settings.inlay_hints_hide_redundant_param_names_last_token.tooltip"
|
||||
) { cell(inlay_hints_hide_redundant_param_names_last_token) }
|
||||
}
|
||||
dispatchAutodetect(false)
|
||||
fancyRow(
|
||||
"settings.warn_style.label",
|
||||
"settings.warn_style.tooltip"
|
||||
) { cell(warn_style) }
|
||||
fancyRow(
|
||||
"settings.highlight_global_var_declarations.label",
|
||||
"settings.highlight_global_var_declarations.tooltip"
|
||||
) { cell(highlight_global_var_declarations) }
|
||||
fancyRow(
|
||||
"settings.skip_std_references.label",
|
||||
"settings.skip_std_references.tooltip"
|
||||
) { cell(skip_std_references) }
|
||||
fancyRow(
|
||||
"settings.prefer_ast_check_as_child_process.label",
|
||||
"settings.prefer_ast_check_as_child_process.tooltip"
|
||||
) { cell(prefer_ast_check_as_child_process) }
|
||||
fancyRow(
|
||||
"settings.builtin_path.label",
|
||||
"settings.builtin_path.tooltip"
|
||||
) { cell(builtin_path).resizableColumn().align(AlignX.FILL) }
|
||||
fancyRow(
|
||||
"settings.build_runner_path.label",
|
||||
"settings.build_runner_path.tooltip"
|
||||
) { cell(build_runner_path).resizableColumn().align(AlignX.FILL) }
|
||||
fancyRow(
|
||||
"settings.global_cache_path.label",
|
||||
"settings.global_cache_path.tooltip"
|
||||
) { cell(global_cache_path).resizableColumn().align(AlignX.FILL) }
|
||||
}
|
||||
|
||||
override fun direnvChanged(state: Boolean) {
|
||||
direnv = state
|
||||
dispatchAutodetect(true)
|
||||
override fun isModified(elem: ZLSSettings): Boolean {
|
||||
return elem != data
|
||||
}
|
||||
|
||||
override var data
|
||||
get() = if (project.isDefault) ZLSSettings() else ZLSSettings(
|
||||
zlsPath.text,
|
||||
override fun apply(elem: ZLSSettings): ZLSSettings? {
|
||||
return data
|
||||
}
|
||||
|
||||
override fun reset(elem: ZLSSettings?) {
|
||||
data = elem ?: ZLSSettings()
|
||||
}
|
||||
|
||||
private var data
|
||||
get() = ZLSSettings(
|
||||
zlsConfigPath.text,
|
||||
inlayHints.isSelected,
|
||||
enable_snippets.isSelected,
|
||||
|
@ -251,7 +196,6 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
|
|||
global_cache_path.text?.ifBlank { null },
|
||||
)
|
||||
set(value) {
|
||||
zlsPath.text = value.zlsPath
|
||||
zlsConfigPath.text = value.zlsConfigPath
|
||||
inlayHints.isSelected = value.inlayHints
|
||||
enable_snippets.isSelected = value.enable_snippets
|
||||
|
@ -275,72 +219,10 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
|
|||
builtin_path.text = value.builtin_path ?: ""
|
||||
build_runner_path.text = value.build_runner_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() {
|
||||
debounce?.cancel("Disposed")
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
zlsConfigPath.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -20,24 +20,26 @@
|
|||
* 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.ProjectManager
|
||||
import java.util.UUID
|
||||
|
||||
class ZigCoreProjectConfigurationProvider: ZigProjectConfigurationProvider {
|
||||
override fun handleMainConfigChanged(project: Project) {
|
||||
}
|
||||
fun <T: ZigToolchain> T.withZLS(uuid: UUID?): T {
|
||||
return withExtraData("zls_uuid", uuid?.asString())
|
||||
}
|
||||
|
||||
override fun createConfigurable(project: Project): SubConfigurable {
|
||||
return ZigProjectConfigurable(project)
|
||||
}
|
||||
val ZigToolchain.zlsUUID: UUID? get() {
|
||||
return extraData["zls_uuid"]?.asUUID()
|
||||
}
|
||||
|
||||
override fun createNewProjectSettingsPanel(holder: ZigProjectConfigurationProvider.SettingsPanelHolder): ZigProjectConfigurationProvider.SettingsPanel {
|
||||
return ZigProjectSettingsPanel(holder, ProjectManager.getInstance().defaultProject)
|
||||
}
|
||||
val ZigToolchain.zls: ZLSVersion? get() {
|
||||
return zlsUUID?.let { zlsInstallations[it] }
|
||||
}
|
||||
|
||||
override val priority: Int
|
||||
get() = 0
|
||||
}
|
||||
val Project.zls: ZLSVersion? get() = ZigToolchainService.getInstance(this).toolchain?.zls
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Add table
Reference in a new issue