Compare commits

..

2 commits

193 changed files with 4995 additions and 6878 deletions

View file

@ -17,199 +17,8 @@ Changelog structure reference:
## [Unreleased]
## [25.2.0]
### Added
- Debugger
- Notify the user if zig run / zig test debugging starts, but a build.zig is present
### Changed
- Project
- Line marker task suggestions for main/test now defer to Zig Build if build.zig file is detected.
### Fixed
- Debugger
- Compilation failures did not open the terminal properly and suppressed the error message
## [25.1.0]
### Added
- IDEA 2025.1 support
- LSP
- Configurable inlay hints file size limit to reduce IDE lag
## [25.0.2]
### Fixed
- Project
- ZLS settings not scrollable in the language server list
## [25.0.1]
### Fixed
- Project
- Zig.iml file created in every project
### Changed
- Project
- BREAKING MAJOR UPDATE: Fully reworked toolchain and language server management
The configuration menu is now very similar to the intellij java toolchain management,
with proper toolchain selection, detection, downloading, etc. This change will require
you to re-configure your toolchains!
- Zig external library root is now no longer shown if zig is not configured
## [24.0.1]
### Added
- Project, Debugging
- TTY support for zig processes
### Removed
- Project
- "Emulate terminal" and "colored output" config options have been removed from zig run/test/build tasks, as they are no longer required for ZigBrains to work.
### Fixed
- Debugger
- Build errors didn't get shown in the console
- Project
- File path browse buttons in zig run configurations didn't work
- Occasional GUI deadlocks
- Zig
- IPC wrapper wasn't passing exit code
## [23.1.2]
### Fixed
- LSP
- IDE warning when renaming symbols
## [23.1.1]
### Fixed
- Project
- New project creation creates a blank ZLS config
## [23.1.0]
### Added
- Project
- Support running file main/tests with hotkey (default: ctrl+shift+f10)
### Changed
- Direnv
- Centralized all direnv toggling into a single project-level option
## [23.0.2]
### Fixed
- Zig
- Documentation comment after regular comment was being highlighted as regular comment
## [23.0.1]
### Fixed
- Project
- mkfifo/bash for zig progress visualization is now detected more reliably (fixes error on macOS)
- Deadlock when launching zig build tasks
## [23.0.0]
### Added
- Project
- Zig std.Progress visualization in the zig tool window (Linux/macOS only)
### Removed
- Project
- Executable / Library new project templates temporarily removed until zig stabilizes
## [22.0.1]
### Fixed
- LSP
- Changing ZLS configs would not restart ZLS
- Project
- Occasional "AWT events are not allowed inside write action" error coming from LSP
- IllegalStateException coming from the standard library handler
## [22.0.0]
### Added
- LSP
- Error/Warning banner at the top of the editor when ZLS is misconfigured/not running
- ZLS version indicator in the zig settings
- Toolchain
- More descriptive error messages when toolchain detection fails
### Changed
- Project
- !!BREAKING CHANGE!! Changed file format of zig tasks to store command line arguments as strings instead of string lists.
This (and newer) versions of the plugin will automatically upgrade tasks from 21.1.0 and before.
### Fixed
- Debugging
- Breakpoints could not be placed inside zig code in Android Studio
- Project
- Zig run/debug configuration command line arguments would lose quotes around arguments
## [21.1.0]
### Added
- Zon
- ZLS integration
### Changed
- Zon
- Fully refactored the parser for parity with the zig parser
## [21.0.0]
### Added
- Zig
- Changing the zig standard library path in the project settings now properly updates the dependency
- ZLS
- All of the config options are now exposed in the GUI
### Changed
- Project
- New project panel is now much more compact
### Fixed
- Zig
- `zig env` failure causes an IDE error
- A local toolchain disappearing (std directory or zig exe deleted) is now handled properly
- LSP4IJ is no longer a hard dependency, ZLS support is now an optional feature.
## [20.3.0]

53
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,53 @@
# Code of Merit
1. The project creators, lead developers, core team, constitute
the managing members of the project and have final say in every decision
of the project, technical or otherwise, including overruling previous decisions.
There are no limitations to this decisional power.
2. Contributions are an expected result of your membership on the project.
Don't expect others to do your work or help you with your work forever.
3. All members have the same opportunities to seek any challenge they want
within the project.
4. Authority or position in the project will be proportional
to the accrued contribution. Seniority must be earned.
5. Software is evolutive: the better implementations must supersede lesser
implementations. Technical advantage is the primary evaluation metric.
6. This is a space for technical prowess; topics outside of the project
will not be tolerated.
7. Non technical conflicts will be discussed in a separate space. Disruption
of the project will not be allowed.
8. Individual characteristics, including but not limited to,
body, sex, sexual preference, race, language, religion, nationality,
or political preferences are irrelevant in the scope of the project and
will not be taken into account concerning your value or that of your contribution
to the project.
9. Discuss or debate the idea, not the person.
10. There is no room for ambiguity: Ambiguity will be met with questioning;
further ambiguity will be met with silence. It is the responsibility
of the originator to provide requested context.
11. If something is illegal outside the scope of the project, it is illegal
in the scope of the project. This Code of Merit does not take precedence over
governing law.
12. This Code of Merit governs the technical procedures of the project not the
activities outside of it.
13. Participation on the project equates to agreement of this Code of Merit.
14. No objectives beyond the stated objectives of this project are relevant
to the project. Any intent to deviate the project from its original purpose
of existence will constitute grounds for remedial action which may include
expulsion from the project.
This document is adapted from the Code of Merit, version 1.0.
See: https://codeofmerit.org/.

View file

@ -25,11 +25,6 @@ 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.
--------------------------------
@ -42,5 +37,4 @@ 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/ZLS.LICENSE
- licenses/LGPL3.LICENSE

View file

@ -15,7 +15,6 @@ through the built-in plugin browser:
1. Go to `Settings -> Plugins`
2. To the right of the `Installed` button at the top, click on the `...` dropdown menu, then select `Manage Plugin Repositories...`
3. Click the add button, and then enter the ZigBrains updater URL, based on your IDE version:
- `2025.1.*` or newer: https://falsepattern.com/zigbrains/updatePlugins-251.xml
- `2024.3.*`: https://falsepattern.com/zigbrains/updatePlugins-243.xml
- `2024.2.*`: https://falsepattern.com/zigbrains/updatePlugins-242.xml
- `2024.1.*`: https://falsepattern.com/zigbrains/updatePlugins-241.xml
@ -39,7 +38,6 @@ and might as well utilize the full semver string for extra information.
## Supporters
- ### [Techatrix](https://github.com/Techatrix)
- ### [nuxusr](https://github.com/nuxusr)
- gree7
- xceno
- AnErrupTion
@ -72,7 +70,11 @@ excellent example on how to write debugger support that doesn't depend on CLion.
<!-- Plugin description -->
Adds support for the Zig Language, utilizing the ZLS language server for advanced coding assistance.
Before you can properly use the plugin, you need to select or download the Zig toolchain and language server in `Settings` -> `Languages & Frameworks` -> `Zig`.
## Quick setup guide for Zig and ZLS
1. Download the latest version of Zig from https://ziglang.org/download
2. Download and compile the ZLS language server, available at https://github.com/zigtools/zls
3. Go to `Settings` -> `Languages & Frameworks` -> `Zig`, and point the `Toolchain Location` and `ZLS path` to the correct places
## Debugging
@ -86,7 +88,6 @@ Debugging Zig code is supported in any native debugging capable IDE. The followi
- RustRover (including the non-commercial free version too)
- GoLand
- PyCharm Professional
- Android Studio
Additionally, in CLion, the plugin uses the C++ Toolchains for sourcing the debugger (this can be toggled off in the settings).

View file

@ -6,15 +6,15 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
plugins {
kotlin("jvm") version "2.1.10" apply false
kotlin("plugin.serialization") version "2.1.10" apply false
id("org.jetbrains.intellij.platform") version "2.5.0"
kotlin("jvm") version "2.0.21" apply false
kotlin("plugin.serialization") version "2.0.21" apply false
id("org.jetbrains.intellij.platform") version "2.2.1"
id("org.jetbrains.changelog") version "2.2.1"
id("org.jetbrains.grammarkit") version "2022.3.2.2" apply false
idea
`maven-publish`
}
val publishVersions = listOf("241", "242", "243", "251")
val publishVersions = listOf("241", "242", "243")
val pluginVersionFull get() = "$pluginVersion-$pluginSinceBuild"
val pluginVersion: String by project
val pluginSinceBuild: String by project
@ -60,6 +60,7 @@ tasks {
allprojects {
idea {
module {
isDownloadJavadoc = false
isDownloadSources = true
}
}
@ -104,7 +105,7 @@ dependencies {
pluginVerifier()
zipSigner()
plugin(lsp4ijPluginString)
// plugin(lsp4ijPluginString)
}
runtimeOnly(project(":core"))
@ -205,7 +206,6 @@ publishVersions.forEach {
archiveFile = distFile(it)
token = providers.environmentVariable("IJ_PUBLISH_TOKEN")
channels = if (pluginVersion.contains("-")) listOf("nightly") else listOf("default")
setDependsOn(dependsOn.filter { if (it is TaskProvider<*>) it.name != "signPlugin" && it.name != "buildPlugin" else true })
}
tasks.named("publish").configure {
dependsOn("jbpublish-$it")

View file

@ -23,7 +23,7 @@
set -e
declare -a branches=("master" "243" "242" "241")
declare -a branches=("master" "242" "241")
DEFAULT_BRANCH="${branches[0]}"

View file

@ -14,8 +14,6 @@ sourceSets["main"].resources.srcDir(genOutputDir)
tasks {
register<Download>("downloadProps") {
onlyIfModified(true)
useETag(true)
src("https://falsepattern.com/zigbrains/msvc.properties")
dest(genOutputDir.map { it.file("msvc.properties") })
}
@ -29,9 +27,7 @@ dependencies {
create(IntelliJPlatformType.CLion, clionVersion, useInstaller = useInstaller)
bundledPlugins("com.intellij.clion", "com.intellij.cidr.base", "com.intellij.nativeDebug")
}
implementation(project(":core")) {
isTransitive = false
}
implementation(project(":core"))
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.debug:$lsp4jVersion") {
exclude("org.eclipse.lsp4j", "org.eclipse.lsp4j")
exclude("org.eclipse.lsp4j", "org.eclipse.lsp4j.jsonrpc")

View file

@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.clion
import com.falsepattern.zigbrains.debugger.ZigDebuggerDriverConfigurationProvider
import com.falsepattern.zigbrains.debugger.settings.ZigDebuggerSettings
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SystemInfo
@ -57,9 +58,8 @@ class ZigClionDebuggerDriverConfigurationProvider: ZigDebuggerDriverConfiguratio
}
return when(toolchain.debuggerKind) {
CPPDebugger.Kind.BUNDLED_GDB,
CPPDebugger.Kind.CUSTOM_GDB -> CLionGDBDriverConfiguration(project, toolchain, isEmulateTerminal = emulateTerminal)
CPPDebugger.Kind.BUNDLED_LLDB,
CPPDebugger.Kind.CUSTOM_LLDB -> CLionLLDBDriverConfiguration(project, toolchain, isEmulateTerminal = emulateTerminal)
CPPDebugger.Kind.CUSTOM_GDB -> CLionGDBDriverConfiguration(project, toolchain)
CPPDebugger.Kind.BUNDLED_LLDB -> CLionLLDBDriverConfiguration(project, toolchain)
}
}
}

View file

@ -27,7 +27,6 @@ import com.falsepattern.zigbrains.debugger.toolchain.*
import com.falsepattern.zigbrains.debugger.win.MSVCDriverConfiguration
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.zig.ZigLanguage
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DoNotAskOption
import com.intellij.openapi.ui.MessageDialogBuilder
@ -86,7 +85,7 @@ private suspend fun availabilityCheck(project: Project, kind: DebuggerKind): Boo
}
if (downloadDebugger) {
val result = withEDTContext(ModalityState.any()) {
val result = withEDTContext {
service.downloadDebugger(project, kind)
}
if (result is ZigDebuggerToolchainService.DownloadResult.Ok) {
@ -105,7 +104,7 @@ private suspend fun showDialog(project: Project, message: String, action: String
}
}
return withEDTContext(ModalityState.any()) {
return withEDTContext {
MessageDialogBuilder
.okCancel(ZigDebugBundle.message("debugger.run.unavailable"), message)
.yesText(action)

View file

@ -22,10 +22,9 @@
package com.falsepattern.zigbrains.debugger
import com.intellij.execution.filters.Filter
import com.intellij.execution.filters.TextConsoleBuilder
import com.intellij.xdebugger.XDebugSession
import com.jetbrains.cidr.execution.RunParameters
import com.jetbrains.cidr.execution.debugger.CidrLocalDebugProcess
class ZigLocalDebugProcess(parameters: RunParameters, session: XDebugSession, consoleBuilder: TextConsoleBuilder) : CidrLocalDebugProcess(parameters, session, consoleBuilder, { Filter.EMPTY_ARRAY }, true)
class ZigLocalDebugProcess(parameters: RunParameters, session: XDebugSession, consoleBuilder: TextConsoleBuilder) : CidrLocalDebugProcess(parameters, session, consoleBuilder)

View file

@ -26,7 +26,11 @@ import com.falsepattern.zigbrains.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.process.*
import com.intellij.execution.process.BaseProcessHandler
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.process.ProcessOutputType
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.KeyWithDefaultValue
import com.intellij.openapi.util.io.toNioPathOrNull
@ -41,12 +45,9 @@ import com.jetbrains.cidr.execution.debugger.memory.AddressRange
import com.jetbrains.cidr.system.HostMachine
import com.jetbrains.cidr.system.LocalHost
import io.ktor.util.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.*
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.future.asDeferred
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.eclipse.lsp4j.debug.*
import org.eclipse.lsp4j.debug.services.IDebugProtocolClient
import org.eclipse.lsp4j.debug.services.IDebugProtocolServer
@ -57,7 +58,8 @@ import java.io.ByteArrayOutputStream
import java.io.File
import java.io.OutputStream
import java.io.PipedOutputStream
import java.util.*
import java.lang.Exception
import java.util.TreeMap
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import kotlin.math.min

View file

@ -39,7 +39,7 @@ class ZigExecConfigBinary(project: Project, factory: ConfigurationFactory) : Zig
get() = ZigDebugBundle.message("configuration.binary.suggested-name")
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
return args.argsSplit()
return args.args
}
override fun getConfigurables(): List<ZigConfigurable<*>> {

View file

@ -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.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
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: ZigToolchain, debug: Boolean): GeneralCommandLine {
override suspend fun getCommandLine(toolchain: AbstractZigToolchain, debug: Boolean): GeneralCommandLine {
val cli = GeneralCommandLine()
val cfg = configuration
cfg.workingDirectory.path?.let { cli.withWorkingDirectory(it) }

View file

@ -23,7 +23,6 @@
package com.falsepattern.zigbrains.debugger.runner.base
import com.falsepattern.zigbrains.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.shared.cli.startIPCAwareProcess
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessEvent
@ -31,7 +30,6 @@ import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.ui.ConsoleView
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.openapi.project.Project
import com.intellij.platform.util.progress.withProgressText
import com.intellij.util.io.awaitExit
import kotlinx.coroutines.Dispatchers
@ -41,17 +39,17 @@ import kotlinx.coroutines.withContext
class PreLaunchProcessListener(val console: ConsoleView) : ProcessListener {
var isBuildFailed: Boolean = false
private set
lateinit var processHandler: ZigProcessHandler.IPCAware
lateinit var processHandler: ProcessHandler
private set
@Throws(ExecutionException::class)
suspend fun executeCommandLineWithHook(project: Project, commandLine: GeneralCommandLine): Boolean {
suspend fun executeCommandLineWithHook(commandLine: GeneralCommandLine): Boolean {
return withProgressText(commandLine.commandLineString) {
val processHandler = commandLine.startIPCAwareProcess(project)
val processHandler = ZigProcessHandler(commandLine)
this@PreLaunchProcessListener.processHandler = processHandler
hook(processHandler)
processHandler.startNotify()
withContext(Dispatchers.IO) {
withContext(Dispatchers.Default) {
processHandler.process.awaitExit()
}
runInterruptible {
@ -68,6 +66,10 @@ class PreLaunchProcessListener(val console: ConsoleView) : ProcessListener {
override fun processTerminated(event: ProcessEvent) {
if (event.exitCode != 0) {
console.print(
"Process finished with exit code " + event.exitCode,
ConsoleViewContentType.NORMAL_OUTPUT
)
isBuildFailed = true
} else {
isBuildFailed = false

View file

@ -23,10 +23,9 @@
package com.falsepattern.zigbrains.debugger.runner.base
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PtyCommandLine
import com.jetbrains.cidr.execution.Installer
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
@ -34,13 +33,13 @@ import java.io.File
class ZigDebugEmitBinaryInstaller<ProfileState: ZigProfileState<*>>(
private val profileState: ProfileState,
private val toolchain: ZigToolchain,
private val toolchain: AbstractZigToolchain,
private val executableFile: File,
private val exeArgs: List<String>
): Installer {
override fun install(): GeneralCommandLine {
val cfg = profileState.configuration
val cli = PtyCommandLine().withConsoleMode(false).withExePath(executableFile.absolutePath)
val cli = GeneralCommandLine().withExePath(executableFile.absolutePath)
cfg.workingDirectory.path?.let { x -> cli.withWorkingDirectory(x) }
cli.addParameters(exeArgs)
cli.withCharset(Charsets.UTF_8)

View file

@ -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.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
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: ZigToolchain,
protected val toolchain: AbstractZigToolchain,
protected val profileState: ProfileState
): RunParameters() {
override fun getDebuggerDriverConfiguration(): DebuggerDriverConfiguration {

View file

@ -24,10 +24,9 @@ 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.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.intellij.execution.ExecutionException
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.platform.util.progress.withProgressText
import com.intellij.util.containers.orNull
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
@ -37,11 +36,10 @@ import java.io.File
import java.nio.file.Files
import kotlin.io.path.absolutePathString
import kotlin.io.path.isExecutable
import kotlin.io.path.pathString
abstract class ZigDebugParametersEmitBinaryBase<ProfileState: ZigProfileState<*>>(
driverConfiguration: DebuggerDriverConfiguration,
toolchain: ZigToolchain,
toolchain: AbstractZigToolchain,
profileState: ProfileState,
) : ZigDebugParametersBase<ProfileState>(driverConfiguration, toolchain, profileState), PreLaunchAware {
@Volatile
@ -51,14 +49,13 @@ abstract class ZigDebugParametersEmitBinaryBase<ProfileState: ZigProfileState<*>
@Throws(ExecutionException::class)
private suspend fun compileExe(listener: PreLaunchProcessListener): File {
val commandLine = profileState.getCommandLine(toolchain, true)
val cliString = commandLine.getCommandLineString(commandLine.exePath.toNioPathOrNull()?.fileName?.pathString)
val tmpDir = FileUtil.createTempDirectory("zigbrains_debug", "", true).toPath()
val exe = tmpDir.resolve("executable")
commandLine.addParameters("-femit-bin=${exe.absolutePathString()}")
if (listener.executeCommandLineWithHook(profileState.environment.project, commandLine))
throw ExecutionException(ZigDebugBundle.message("debug.base.compile.failed.generic", cliString))
if (listener.executeCommandLineWithHook(commandLine))
throw ExecutionException(ZigDebugBundle.message("debug.base.compile.failed.generic"))
return withContext(Dispatchers.IO) {
Files.list(tmpDir).use { stream ->

View file

@ -24,10 +24,9 @@ package com.falsepattern.zigbrains.debugger.runner.base
import com.falsepattern.zigbrains.debugbridge.ZigDebuggerDriverConfigurationProviderBase
import com.falsepattern.zigbrains.debugger.ZigLocalDebugProcess
import com.falsepattern.zigbrains.debugger.runner.build.ZigDebugRunnerBuild
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.run.ZigProgramRunner
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.coroutine.runInterruptibleEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.intellij.execution.DefaultExecutionResult
@ -41,8 +40,6 @@ import com.intellij.execution.runners.RunContentBuilder
import com.intellij.execution.ui.ConsoleView
import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.execution.ui.RunContentDescriptor
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.guessProjectDir
import com.intellij.platform.util.progress.reportProgress
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugProcessStarter
@ -54,13 +51,13 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
@Throws(ExecutionException::class)
override suspend fun execute(
state: ProfileState,
toolchain: ZigToolchain,
toolchain: AbstractZigToolchain,
environment: ExecutionEnvironment
): RunContentDescriptor? {
val project = environment.project
val driverProviders = ZigDebuggerDriverConfigurationProviderBase.EXTENSION_POINT_NAME.extensionList
for (provider in driverProviders) {
val driver = provider.getDebuggerConfiguration(project, isElevated = false, emulateTerminal = true, DebuggerDriverConfiguration::class.java) ?: continue
val driver = provider.getDebuggerConfiguration(project, isElevated = false, emulateTerminal = false, DebuggerDriverConfiguration::class.java) ?: continue
return executeWithDriver(state, toolchain, environment, driver) ?: continue
}
return null
@ -69,7 +66,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
@Throws(ExecutionException::class)
private suspend fun executeWithDriver(
state: ProfileState,
toolchain: ZigToolchain,
toolchain: AbstractZigToolchain,
environment: ExecutionEnvironment,
debuggerDriver: DebuggerDriverConfiguration
): RunContentDescriptor? {
@ -84,25 +81,22 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
}
} catch (e: ExecutionException) {
console.print("\n", ConsoleViewContentType.ERROR_OUTPUT)
e.message?.let { listener.console.print(it, ConsoleViewContentType.ERROR_OUTPUT) }
if (this !is ZigDebugRunnerBuild && environment.project.guessProjectDir()?.children?.any { it.name == "build.zig" } == true) {
console.print("\n Warning: build.zig file detected in project.\n Did you want to use a Zig Build task instead?", ConsoleViewContentType.ERROR_OUTPUT)
}
e.message?.let { listener.console.print(it, ConsoleViewContentType.SYSTEM_OUTPUT) }
}
if (listener.isBuildFailed) {
val executionResult = DefaultExecutionResult(console, listener.processHandler.unwrap())
return@reportProgress withEDTContext(ModalityState.any()) {
val executionResult = DefaultExecutionResult(console, listener.processHandler)
return@reportProgress withEDTContext {
val runContentBuilder = RunContentBuilder(executionResult, environment)
runContentBuilder.showRunContent(null)
}
}
}
return@reportProgress runInterruptibleEDT(ModalityState.any()) {
return@reportProgress runInterruptibleEDT {
val debuggerManager = XDebuggerManager.getInstance(environment.project)
debuggerManager.startSession(environment, object: XDebugProcessStarter() {
override fun start(session: XDebugSession): XDebugProcess {
val project = session.project
val textConsoleBuilder = state.consoleBuilder
val textConsoleBuilder = SharedConsoleBuilder(console)
val debugProcess = ZigLocalDebugProcess(runParameters, session, textConsoleBuilder)
ProcessTerminatedListener.attach(debugProcess.processHandler, project)
debugProcess.start()
@ -117,7 +111,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
protected abstract fun getDebugParameters(
state: ProfileState,
debuggerDriver: DebuggerDriverConfiguration,
toolchain: ZigToolchain
toolchain: AbstractZigToolchain
): ZigDebugParametersBase<ProfileState>
private class SharedConsoleBuilder(private val console: ConsoleView) : TextConsoleBuilder() {

View file

@ -26,16 +26,16 @@ 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.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
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: ZigToolchain, profileState: ZigProfileStateBinary) :
class ZigDebugParametersBinary @Throws(ExecutionException::class) constructor(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, 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 {
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.args.argsSplit())
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.args.args)
}
}

View file

@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.execution.binary.ZigProfileStateBinar
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.project.toolchain.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: ZigToolchain
toolchain: AbstractZigToolchain
): ZigDebugParametersBase<ZigProfileStateBinary> {
return ZigDebugParametersBinary(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
}

View file

@ -28,7 +28,7 @@ import com.falsepattern.zigbrains.debugger.runner.base.PreLaunchProcessListener
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugEmitBinaryInstaller
import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugParametersBase
import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.intellij.execution.ExecutionException
import com.intellij.openapi.util.SystemInfo
import com.intellij.platform.util.progress.withProgressText
@ -46,14 +46,14 @@ import kotlin.io.path.isRegularFile
class ZigDebugParametersBuild(
driverConfiguration: DebuggerDriverConfiguration,
toolchain: ZigToolchain,
toolchain: AbstractZigToolchain,
profileState: ZigProfileStateBuild
) : ZigDebugParametersBase<ZigProfileStateBuild>(driverConfiguration, toolchain, profileState), PreLaunchAware {
@Volatile
private lateinit var executableFile: File
override fun getInstaller(): Installer {
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit())
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.args)
}
@Throws(ExecutionException::class)
@ -61,8 +61,8 @@ class ZigDebugParametersBuild(
withProgressText("Building zig project") {
withContext(Dispatchers.IO) {
val commandLine = profileState.getCommandLine(toolchain, true)
if (listener.executeCommandLineWithHook(profileState.environment.project, commandLine))
return@withContext
if (listener.executeCommandLineWithHook(commandLine))
throw ExecutionException(ZigDebugBundle.message("debug.build.compile.failed.generic"))
val cfg = profileState.configuration
val workingDir = cfg.workingDirectory.path
val exe = profileState.configuration.exePath.path ?: run {

View file

@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.execution.build.ZigExecConfigBuild
import com.falsepattern.zigbrains.project.execution.build.ZigProfileStateBuild
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.project.toolchain.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: ZigToolchain
toolchain: AbstractZigToolchain
): ZigDebugParametersBase<ZigProfileStateBuild> {
return ZigDebugParametersBuild(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
}

View file

@ -25,13 +25,13 @@ 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.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.jetbrains.cidr.execution.Installer
import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: ZigToolchain, profileState: ZigProfileStateRun) :
class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateRun) :
ZigDebugParametersEmitBinaryBase<ZigProfileStateRun>(driverConfiguration, toolchain, profileState) {
override fun getInstaller(): Installer {
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit())
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.args)
}
}

View file

@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.execution.run.ZigExecConfigRun
import com.falsepattern.zigbrains.project.execution.run.ZigProfileStateRun
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.project.toolchain.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: ZigToolchain
toolchain: AbstractZigToolchain
): ZigDebugParametersBase<ZigProfileStateRun> {
return ZigDebugParametersRun(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
}

View file

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

View file

@ -27,8 +27,8 @@ import com.falsepattern.zigbrains.debugger.runner.base.ZigDebugRunnerBase
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.execution.test.ZigExecConfigTest
import com.falsepattern.zigbrains.project.execution.test.ZigProfileStateTest
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.project.toolchain.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: ZigToolchain
toolchain: AbstractZigToolchain
): ZigDebugParametersBase<ZigProfileStateTest> {
return ZigDebugParametersTest(debuggerDriver, LocalZigToolchain.ensureLocal(toolchain), state)
}

View file

@ -31,7 +31,6 @@ import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.ide.plugins.PluginManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.observable.util.whenItemSelected
import com.intellij.openapi.ui.ComboBox
@ -89,7 +88,7 @@ class ZigDebuggerToolchainConfigurableUi : ZigDebuggerUiComponent {
row(ZigDebugBundle.message("settings.debugger.toolchain.debugger.label")) {
comment = cell(debuggerKindComboBox)
.comment("", DEFAULT_COMMENT_WIDTH) {
zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) {
zigCoroutineScope.launchWithEDT {
withModalProgress(ModalTaskOwner.component(debuggerKindComboBox), "Downloading debugger", TaskCancellation.cancellable()) {
downloadDebugger()
}
@ -97,7 +96,7 @@ class ZigDebuggerToolchainConfigurableUi : ZigDebuggerUiComponent {
}
.applyToComponent {
whenItemSelected(null) {
zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) {
zigCoroutineScope.launchWithEDT {
this@ZigDebuggerToolchainConfigurableUi.update()
}
}
@ -112,7 +111,7 @@ class ZigDebuggerToolchainConfigurableUi : ZigDebuggerUiComponent {
cell(useClion)
}
}
zigCoroutineScope.launchWithEDT(ModalityState.defaultModalityState()) {
zigCoroutineScope.launchWithEDT {
update()
}
}

View file

@ -27,8 +27,10 @@ import com.falsepattern.zigbrains.debugger.settings.MSVCDownloadPermission
import com.falsepattern.zigbrains.debugger.settings.ZigDebuggerSettings
import com.falsepattern.zigbrains.debugger.toolchain.ZigDebuggerToolchainService.Companion.downloadPath
import com.falsepattern.zigbrains.shared.coroutine.withCurrentEDTModalityContext
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.intellij.notification.Notification
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.coroutineToIndicator
import com.intellij.openapi.ui.DialogBuilder
import com.intellij.platform.util.progress.withProgressText

View file

@ -23,7 +23,6 @@
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
@ -35,20 +34,22 @@ 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
import com.jetbrains.cidr.execution.debugger.backend.bin.UrlProvider
import com.jetbrains.cidr.execution.debugger.backend.lldb.LLDBDriverConfiguration
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.net.URL
import java.nio.file.Path
@ -168,9 +169,7 @@ class ZigDebuggerToolchainService {
}
try {
withContext(Dispatchers.IO) {
downloadAndUnArchive(baseDir, downloadableBinaries)
}
downloadAndUnArchive(baseDir, downloadableBinaries)
return DownloadResult.Ok(baseDir)
} catch (e: IOException) {
//TODO logging
@ -209,40 +208,34 @@ class ZigDebuggerToolchainService {
@Throws(IOException::class)
@RequiresEdt
private suspend fun downloadAndUnArchive(baseDir: Path, binariesToDownload: List<DownloadableDebuggerBinary>) {
reportSequentialProgress { reporter ->
val service = DownloadableFileService.getInstance()
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 = 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)
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 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)
}
private fun lldbUrls(): Pair<URL, URL>? {
@ -337,6 +330,38 @@ 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()

View file

@ -39,8 +39,9 @@ import org.eclipse.lsp4j.jsonrpc.debug.messages.DebugResponseMessage
import org.eclipse.lsp4j.jsonrpc.messages.Message
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
import java.io.InputStream
import java.lang.RuntimeException
import java.security.MessageDigest
import java.util.*
import java.util.Base64
import java.util.concurrent.CompletableFuture
import java.util.zip.Inflater

View file

@ -1,5 +1,5 @@
<idea-plugin package="com.falsepattern.zigbrains.debugger">
<depends>com.intellij.modules.cidr.debugger</depends>
<depends>com.intellij.nativeDebug</depends>
<extensions defaultExtensionNs="com.intellij">
<configurationType

View file

@ -15,7 +15,7 @@ debugger.run.unavailable.reason.download.button=Download
debugger.run.unavailable.reason.update=Debugger is outdated
debugger.run.unavailable.reason.update.button=Update
debug.build.compile.failed.boilerplate={0}\nPlease edit this intellij build configuration and specify the path of the executable created by "zig build" directly!
debug.base.compile.failed.generic=Failed to compile executable with command: {0}
debug.base.compile.failed.generic=Failed to compile executable
debug.base.compile.failed.no-exe=Failed to find compiled binary
debug.build.compile.failed.multiple-exe=Multiple compiled binaries found
debug.build.compile.failed.no-workdir=Cannot find working directory to run debugged executable

View file

@ -9,15 +9,12 @@ plugins {
val ideaCommunityVersion: String by project
val useInstaller = property("useInstaller").toString().toBoolean()
val serializationVersion: String by project
dependencies {
intellijPlatform {
create(IntelliJPlatformType.IntellijIdeaCommunity, ideaCommunityVersion, useInstaller = useInstaller)
}
compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:$serializationVersion") {
isTransitive = false
}
compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-core:1.7.3")
}
//region grammars

View file

@ -175,6 +175,17 @@
//Mixins
mixin("StringLiteral")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigStringLiteralMixinImpl"
implements("StringLiteral")="com.falsepattern.zigbrains.zig.psi.mixins.ZigStringLiteralMixin"
mixin("PrimaryTypeExpr")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigPrimaryTypeExprMixinImpl"
implements("PrimaryTypeExpr")="com.falsepattern.zigbrains.zig.psi.mixins.ZigPrimaryTypeExprMixin"
implements("Expr")="com.falsepattern.zigbrains.zig.psi.mixins.ZigExprMixin"
mixin("FnDeclProto")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigFnDeclProtoMixinImpl"
implements("FnDeclProto")="com.falsepattern.zigbrains.zig.psi.mixins.ZigFnDeclProtoMixin"
mixin("VarDeclProto")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigVarDeclProtoMixinImpl"
implements("VarDeclProto")="com.falsepattern.zigbrains.zig.psi.mixins.ZigVarDeclProtoMixin"
mixin("ContainerMembers")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigContainerMembersMixinImpl"
implements("ContainerMembers")="com.falsepattern.zigbrains.zig.psi.mixins.ZigContainerMembersMixin"
mixin("ParamDecl")="com.falsepattern.zigbrains.zig.psi.impl.mixins.ZigParamDeclMixinImpl"
implements("ParamDecl")="com.falsepattern.zigbrains.zig.psi.mixins.ZigParamDeclMixin"
}
Root ::= CONTAINER_DOC_COMMENT? ContainerMembers?
@ -189,11 +200,13 @@ TestDecl ::= KEYWORD_TEST (STRING_LITERAL_SINGLE | IDENTIFIER)? Block {pin=1}
ComptimeDecl ::= KEYWORD_COMPTIME Block
Decl
::= (KEYWORD_EXPORT | KEYWORD_EXTERN STRING_LITERAL_SINGLE? | KEYWORD_INLINE | KEYWORD_NOINLINE)? FnProto (SEMICOLON | Block)
::= (KEYWORD_EXPORT | KEYWORD_EXTERN STRING_LITERAL_SINGLE? | KEYWORD_INLINE | KEYWORD_NOINLINE)? FnDeclProto (SEMICOLON | Block)
| (KEYWORD_EXPORT | KEYWORD_EXTERN STRING_LITERAL_SINGLE?)? KEYWORD_THREADLOCAL? GlobalVarDecl
| KEYWORD_USINGNAMESPACE Expr SEMICOLON
FnProto ::= KEYWORD_FN IDENTIFIER? LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr {pin=1}
FnDeclProto ::= KEYWORD_FN IDENTIFIER LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr {pin=1}
FnTypeProto ::= KEYWORD_FN LPAREN ParamDeclList RPAREN ByteAlign? AddrSpace? LinkSection? CallConv? EXCLAMATIONMARK? TypeExpr {pin=1}
VarDeclProto ::= (KEYWORD_CONST | KEYWORD_VAR) IDENTIFIER (COLON TypeExpr)? ByteAlign? AddrSpace? LinkSection? {pin=1}
@ -337,7 +350,7 @@ PrimaryTypeExpr
| DOT InitList
| ErrorSetDecl
| FLOAT
| FnProto
| FnTypeProto
| GroupedExpr
| LabeledTypeExpr
| IDENTIFIER

View file

@ -34,12 +34,9 @@ import static com.falsepattern.zigbrains.zig.psi.ZigTypes.*;
%implements FlexLexer
%function advance
%type IElementType
%unicode
WHITE_SPACE=\s+
// visual studio parity
LF=\r\n?|[\n\u0085\u2028\u2029]
CRLF=\R
WHITE_SPACE=[\s]+
bin=[01]
bin_="_"? {bin}
@ -56,13 +53,13 @@ dec_int={dec} {dec_}*
hex_int={hex} {hex_}*
char_char= \\ .
| [^\'\r\n\u0085\u2028\u2029]
| [^\'\n]
string_char= \\ .
| [^\"\r\n\u0085\u2028\u2029]
| [^\"\n]
nl_wrap={LF} (\s|{LF})*
all_no_nl=[^\r\n\u0085\u2028\u2029]+
all_nl_wrap=[^\n]* [ \n]*
all_no_nl=[^\n]+
FLOAT= "0x" {hex_int} "." {hex_int} ([pP] [-+]? {dec_int})?
@ -86,35 +83,30 @@ BUILTINIDENTIFIER="@"[A-Za-z_][A-Za-z0-9_]*
%state UNT_SQUOT
%state UNT_DQUOT
%state CMT_LINE
%state CMT_DOC
%state CMT_CDOC
%state CDOC_CMT
%state DOC_CMT
%state LINE_CMT
%%
//Comments
<YYINITIAL> "//!" { yybegin(CMT_CDOC); }
<YYINITIAL> "////" { yybegin(CMT_LINE); }
<YYINITIAL> "///" { yybegin(CMT_DOC); }
<YYINITIAL> "//" { yybegin(CMT_LINE); }
<YYINITIAL> "//!" { yybegin(CDOC_CMT); }
<CDOC_CMT> {all_nl_wrap} "//!" { }
<CDOC_CMT> {all_no_nl} { }
<CDOC_CMT> \n { yybegin(YYINITIAL); return CONTAINER_DOC_COMMENT; }
<CDOC_CMT> <<EOF>> { yybegin(YYINITIAL); return CONTAINER_DOC_COMMENT; }
<CMT_LINE> {all_no_nl} { }
<CMT_LINE> {nl_wrap} "////" { }
<CMT_LINE> {nl_wrap} "///" { yypushback(yylength()); yybegin(YYINITIAL); return LINE_COMMENT; }
<CMT_LINE> {nl_wrap} "//" { }
<CMT_LINE> {LF} { yybegin(YYINITIAL); return LINE_COMMENT; }
<CMT_LINE> <<EOF>> { yybegin(YYINITIAL); return LINE_COMMENT; }
<YYINITIAL> "///" { yybegin(DOC_CMT); }
<DOC_CMT> {all_nl_wrap} "///" { }
<DOC_CMT> {all_no_nl} { }
<DOC_CMT> \n { yybegin(YYINITIAL); return DOC_COMMENT; }
<DOC_CMT> <<EOF>> { yybegin(YYINITIAL); return DOC_COMMENT; }
<CMT_DOC> {all_no_nl} { }
<CMT_DOC> {nl_wrap} "////" { yypushback(yylength()); yybegin(YYINITIAL); return DOC_COMMENT; }
<CMT_DOC> {nl_wrap} "///" { }
<CMT_DOC> {LF} { yybegin(YYINITIAL); return DOC_COMMENT; }
<CMT_DOC> <<EOF>> { yybegin(YYINITIAL); return DOC_COMMENT; }
<CMT_CDOC> {all_no_nl} { }
<CMT_CDOC> {nl_wrap} "//!" { }
<CMT_CDOC> {LF} { yybegin(YYINITIAL); return CONTAINER_DOC_COMMENT; }
<CMT_CDOC> <<EOF>> { yybegin(YYINITIAL); return CONTAINER_DOC_COMMENT; }
<YYINITIAL> "//" { yybegin(LINE_CMT); }
<LINE_CMT> {all_nl_wrap} "//" { }
<LINE_CMT> {all_no_nl} { }
<LINE_CMT> \n { yybegin(YYINITIAL); return LINE_COMMENT; }
<LINE_CMT> <<EOF>> { yybegin(YYINITIAL); return LINE_COMMENT; }
//Symbols
<YYINITIAL> "&" { return AMPERSAND; }
@ -234,31 +226,24 @@ BUILTINIDENTIFIER="@"[A-Za-z_][A-Za-z0-9_]*
<YYINITIAL> "volatile" { return KEYWORD_VOLATILE; }
<YYINITIAL> "while" { return KEYWORD_WHILE; }
//Strings
<YYINITIAL> "'" { yybegin(CHAR_LIT); }
<CHAR_LIT> {char_char}*"'" { yybegin(YYINITIAL); return CHAR_LITERAL; }
<CHAR_LIT> <<EOF>> { yybegin(YYINITIAL); return BAD_SQUOT; }
<CHAR_LIT> [^] { yypushback(1); yybegin(UNT_SQUOT); }
<YYINITIAL> {FLOAT} { return FLOAT; }
<YYINITIAL> {INTEGER} { return INTEGER; }
<YYINITIAL> "\"" { yybegin(STR_LIT); }
<STR_LIT> {string_char}*"\"" { yybegin(YYINITIAL); return STRING_LITERAL_SINGLE; }
<STR_LIT> <<EOF>> { yybegin(YYINITIAL); return BAD_DQUOT; }
<STR_LIT> [^] { yypushback(1); yybegin(UNT_DQUOT); }
<YYINITIAL> "\\\\" { yybegin(STR_MULT_LINE); }
<STR_MULT_LINE> {all_no_nl} { }
<STR_MULT_LINE> {nl_wrap} "\\\\" { }
<STR_MULT_LINE> {LF} { yybegin(YYINITIAL); return STRING_LITERAL_MULTI; }
<STR_MULT_LINE> {all_nl_wrap} "\\\\" { }
<STR_MULT_LINE> {all_no_nl} { }
<STR_MULT_LINE> \n { yybegin(YYINITIAL); return STRING_LITERAL_MULTI; }
<STR_MULT_LINE> <<EOF>> { yybegin(YYINITIAL); return STRING_LITERAL_MULTI; }
//Numbers
<YYINITIAL> {FLOAT} { return FLOAT; }
<YYINITIAL> {INTEGER} { return INTEGER; }
//Identifiers
<YYINITIAL> {IDENTIFIER_PLAIN} { return IDENTIFIER; }
<YYINITIAL> "@\"" { yybegin(ID_QUOT); }
<ID_QUOT> {string_char}*"\"" { yybegin(YYINITIAL); return IDENTIFIER; }
@ -267,17 +252,13 @@ BUILTINIDENTIFIER="@"[A-Za-z_][A-Za-z0-9_]*
<YYINITIAL> {BUILTINIDENTIFIER} { return BUILTINIDENTIFIER; }
//Error handling
<UNT_SQUOT> <<EOF>> { yybegin(YYINITIAL); return BAD_SQUOT; }
<UNT_SQUOT> {LF} { yybegin(YYINITIAL); return BAD_SQUOT; }
<UNT_SQUOT> {CRLF} { yybegin(YYINITIAL); return BAD_SQUOT; }
<UNT_SQUOT> {all_no_nl} { }
<UNT_DQUOT> <<EOF>> { yybegin(YYINITIAL); return BAD_DQUOT; }
<UNT_DQUOT> {LF} { yybegin(YYINITIAL); return BAD_DQUOT; }
<UNT_DQUOT> {CRLF} { yybegin(YYINITIAL); return BAD_DQUOT; }
<UNT_DQUOT> {all_no_nl} { }
//Misc
<YYINITIAL> {WHITE_SPACE} { return WHITE_SPACE; }
[^] { return BAD_CHARACTER; }

View file

@ -34,57 +34,46 @@
elementTypeClass="com.falsepattern.zigbrains.zon.parser.ZonElementType"
tokenTypeClass="com.falsepattern.zigbrains.zon.parser.ZonTokenType"
tokens=[
LINE_COMMENT='comment'
DOT='.'
EQUAL='='
LBRACE='{'
RBRACE='}'
EQ='='
COMMA=','
KEYWORD_FALSE='false'
KEYWORD_TRUE='true'
KEYWORD_NULL='null'
NUM_NAN='nan'
NUM_INF='inf'
CHAR_LITERAL='char literal'
STRING_LITERAL_SINGLE='string literal'
STRING_LITERAL_MULTI='multiline string literal'
FLOAT='float'
INTEGER='integer'
IDENTIFIER='identifier'
BAD_SQUOT='unterminated quote'
BAD_DQUOT='unterminated double quote'
COMMENT='comment'
ID='identifier'
STRING_LITERAL_SINGLE='string'
LINE_STRING='multiline string'
BAD_STRING='unterminated string'
BOOL_TRUE='true'
BOOL_FALSE='false'
]
//Mixins
mixin("entry")="com.falsepattern.zigbrains.zon.psi.impl.mixins.ZonEntryMixinImpl"
implements("entry")="com.falsepattern.zigbrains.zon.psi.mixins.ZonEntryMixin"
mixin("identifier")="com.falsepattern.zigbrains.zon.psi.impl.mixins.ZonIdentifierMixinImpl"
implements("identifier")="com.falsepattern.zigbrains.zon.psi.mixins.ZonIdentifierMixin"
}
Root ::= Expr
zonFile ::= entry
Expr
::= CHAR_LITERAL
| StringLiteral
| DOT IDENTIFIER
| DOT InitList
| Bool
| Number
| KEYWORD_NULL
entry ::= DOT LBRACE (list | struct | ()) RBRACE
struct ::= (property | property_placeholder) (COMMA (property_placeholder? property property_placeholder? | property_placeholder))* COMMA?
InitList
::= LBRACE ZB_InitList_Body RBRACE {pin=1}
list ::= value (COMMA value)* COMMA?
private ZB_InitList_Body
::= FieldInit (COMMA ZB_InitList_FieldInit)* COMMA?
| Expr (COMMA ZB_InitList_Expr)* COMMA?
| ()
property ::= DOT identifier EQ value
private ZB_InitList_FieldInit ::= FieldInit {recoverWhile="ZB_InitList_Recover"}
private ZB_InitList_Expr ::= Expr {recoverWhile="ZB_InitList_Recover"}
identifier ::= ID
private ZB_InitList_Recover ::= !(COMMA | RBRACE)
property_placeholder ::= DOT? INTELLIJ_COMPLETION_DUMMY
FieldInit ::= DOT IDENTIFIER EQUAL Expr
private value ::= entry | boolean | STRING_LITERAL | value_placeholder
Bool ::= KEYWORD_TRUE | KEYWORD_FALSE
value_placeholder ::= INTELLIJ_COMPLETION_DUMMY
Number ::= FLOAT | INTEGER | NUM_NAN | NUM_INF
boolean ::= BOOL_TRUE | BOOL_FALSE
StringLiteral ::= STRING_LITERAL_SINGLE | STRING_LITERAL_MULTI
STRING_LITERAL ::= STRING_LITERAL_SINGLE | LINE_STRING+

View file

@ -36,124 +36,54 @@ import static com.falsepattern.zigbrains.zon.psi.ZonTypes.*;
%type IElementType
%unicode
WHITE_SPACE=\s+
CRLF=\R
WHITE_SPACE=[\s]+
LINE_COMMENT="//" [^\n]* | "////" [^\n]*
COMMENT="///".*
// visual studio parity
LF=\r\n?|[\n\u0085\u2028\u2029]
ID=[A-Za-z_][A-Za-z0-9_]*
bin=[01]
bin_="_"? {bin}
oct=[0-7]
oct_="_"? {oct}
hex=[0-9a-fA-F]
hex_="_"? {hex}
dec=[0-9]
dec_="_"? {dec}
char_escape
= "\\x" {hex} {hex}
| "\\u{" {hex}+ "}"
| "\\" [nr\\t'\"]
bin_int={bin} {bin_}*
oct_int={oct} {oct_}*
dec_int={dec} {dec_}*
hex_int={hex} {hex_}*
string_char
= {char_escape}
| [^\\\"\n]
char_char= \\ .
| [^\'\r\n\u0085\u2028\u2029]
LINE_STRING=("\\\\" [^\n]* [ \n]*)+
string_char= \\ .
| [^\"\r\n\u0085\u2028\u2029]
nl_wrap={LF} (\s|{LF})*
all_no_nl=[^\r\n\u0085\u2028\u2029]+
FLOAT= "0x" {hex_int} "." {hex_int} ([pP] [-+]? {dec_int})?
| {dec_int} "." {dec_int} ([eE] [-+]? {dec_int})?
| "0x" {hex_int} [pP] [-+]? {dec_int}
| {dec_int} [eE] [-+]? {dec_int}
INTEGER= "0b" {bin_int}
| "0o" {oct_int}
| "0x" {hex_int}
| {dec_int}
IDENTIFIER_PLAIN=[A-Za-z_][A-Za-z0-9_]*
%state STR_LIT
%state STR_MULT_LINE
%state CHAR_LIT
%state ID_QUOT
%state UNT_SQUOT
%state UNT_DQUOT
%state CMT_LINE
%state STRING_LITERAL
%state ID_STRING
%state UNCLOSED_STRING
%%
//Comments
<YYINITIAL> "//" { yybegin(CMT_LINE); }
<CMT_LINE> {all_no_nl} { }
<CMT_LINE> {nl_wrap} "//" { }
<CMT_LINE> \R { yybegin(YYINITIAL); return LINE_COMMENT; }
<CMT_LINE> <<EOF>> { yybegin(YYINITIAL); return LINE_COMMENT; }
//Symbols
<YYINITIAL> "." { return DOT; }
<YYINITIAL> "=" { return EQUAL; }
<YYINITIAL> "{" { return LBRACE; }
<YYINITIAL> "}" { return RBRACE; }
<YYINITIAL> "," { return COMMA; }
//Keywords
<YYINITIAL> "false" { return KEYWORD_FALSE; }
<YYINITIAL> "true" { return KEYWORD_TRUE; }
<YYINITIAL> "null" { return KEYWORD_NULL; }
<YYINITIAL> "nan" { return NUM_NAN; }
<YYINITIAL> "inf" { return NUM_INF; }
//Strings
<YYINITIAL> "'" { yybegin(CHAR_LIT); }
<CHAR_LIT> {char_char}*"'" { yybegin(YYINITIAL); return CHAR_LITERAL; }
<CHAR_LIT> <<EOF>> { yybegin(YYINITIAL); return BAD_SQUOT; }
<CHAR_LIT> [^] { yypushback(1); yybegin(UNT_SQUOT); }
<YYINITIAL> "\"" { yybegin(STR_LIT); }
<STR_LIT> {string_char}*"\"" { yybegin(YYINITIAL); return STRING_LITERAL_SINGLE; }
<STR_LIT> <<EOF>> { yybegin(YYINITIAL); return BAD_DQUOT; }
<STR_LIT> [^] { yypushback(1); yybegin(UNT_DQUOT); }
<YYINITIAL> "\\\\" { yybegin(STR_MULT_LINE); }
<STR_MULT_LINE> {all_no_nl} { }
<STR_MULT_LINE> {nl_wrap} "\\\\" { }
<STR_MULT_LINE> {LF} { yybegin(YYINITIAL); return STRING_LITERAL_MULTI; }
<STR_MULT_LINE> <<EOF>> { yybegin(YYINITIAL); return STRING_LITERAL_MULTI; }
//Numbers
<YYINITIAL> {FLOAT} { return FLOAT; }
<YYINITIAL> {INTEGER} { return INTEGER; }
//Identifiers
<YYINITIAL> {IDENTIFIER_PLAIN} { return IDENTIFIER; }
<YYINITIAL> "@\"" { yybegin(ID_QUOT); }
<ID_QUOT> {string_char}*"\"" { yybegin(YYINITIAL); return IDENTIFIER; }
<ID_QUOT> <<EOF>> { yybegin(YYINITIAL); return BAD_DQUOT; }
<ID_QUOT> [^] { yypushback(1); yybegin(UNT_DQUOT); }
//Error handling
<UNT_SQUOT> <<EOF>> { yybegin(YYINITIAL); return BAD_SQUOT; }
<UNT_SQUOT> {LF} { yybegin(YYINITIAL); return BAD_SQUOT; }
<UNT_SQUOT> {all_no_nl} { }
<UNT_DQUOT> <<EOF>> { yybegin(YYINITIAL); return BAD_DQUOT; }
<UNT_DQUOT> {LF} { yybegin(YYINITIAL); return BAD_DQUOT; }
<UNT_DQUOT> {all_no_nl} { }
//Misc
<YYINITIAL> {WHITE_SPACE} { return WHITE_SPACE; }
<YYINITIAL> "." { return DOT; }
<YYINITIAL> "IntellijIdeaRulezzz" { return INTELLIJ_COMPLETION_DUMMY; }
<YYINITIAL> "{" { return LBRACE; }
<YYINITIAL> "}" { return RBRACE; }
<YYINITIAL> "=" { return EQ; }
<YYINITIAL> "," { return COMMA; }
<YYINITIAL> "true" { return BOOL_TRUE; }
<YYINITIAL> "false" { return BOOL_FALSE; }
<YYINITIAL> {COMMENT} { return COMMENT; }
<YYINITIAL> {LINE_COMMENT} { return COMMENT; }
<YYINITIAL> {ID} { return ID; }
<YYINITIAL> "@\"" { yybegin(ID_STRING); }
<ID_STRING> {string_char}*"\"" { yybegin(YYINITIAL); return ID; }
<ID_STRING> [^] { yypushback(1); yybegin(UNCLOSED_STRING); }
<YYINITIAL> "\"" { yybegin(STRING_LITERAL); }
<STRING_LITERAL> {string_char}*"\"" { yybegin(YYINITIAL); return STRING_LITERAL_SINGLE; }
<STRING_LITERAL> [^] { yypushback(1); yybegin(UNCLOSED_STRING); }
<UNCLOSED_STRING>[^\n]*{CRLF} { yybegin(YYINITIAL); return BAD_STRING; }
<YYINITIAL> {LINE_STRING} { return LINE_STRING; }
[^] { return BAD_CHARACTER; }

View file

@ -20,9 +20,12 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.shared.ipc
package com.falsepattern.zigbrains.zig.psi;
import com.intellij.execution.configurations.GeneralCommandLine
import java.nio.file.Path
import com.falsepattern.zigbrains.zig.references.ZigType;
data class IPC(val cli: GeneralCommandLine, val fifoPath: Path, val fifoClose: AutoCloseable)
public interface ZigTypedElement {
default ZigType getType() {
return ZigType.Generic;
}
}

View file

@ -22,6 +22,12 @@
package com.falsepattern.zigbrains
import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
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
@ -33,8 +39,10 @@ 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
@ -67,6 +75,19 @@ 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
}
}
}
}

View file

@ -29,53 +29,23 @@ 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
@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()
object DirenvCmd {
suspend fun importDirenv(project: Project): Env {
if (!direnvInstalled() || !project.isTrusted())
return emptyEnv
val workDir = project.guessProjectDir()?.toNioPath() ?: return emptyEnv
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")
val runOutput = run(project, workDir, "export", "json")
if (runOutput.error) {
if (runOutput.output.contains("is blocked")) {
Notifications.Bus.notify(Notification(
@ -84,7 +54,7 @@ class DirenvService(val project: Project): SerializablePersistentStateComponent<
ZigBrainsBundle.message("notification.content.direnv-blocked"),
NotificationType.ERROR
))
return Env.empty
return emptyEnv
} else {
Notifications.Bus.notify(Notification(
GROUP_DISPLAY_ID,
@ -92,22 +62,22 @@ class DirenvService(val project: Project): SerializablePersistentStateComponent<
ZigBrainsBundle.message("notification.content.direnv-error", runOutput.output),
NotificationType.ERROR
))
return Env.empty
return emptyEnv
}
}
return if (runOutput.output.isBlank()) {
Env.empty
emptyEnv
} else {
Env(Json.decodeFromString<Map<String, String>>(runOutput.output))
}
}
private suspend fun run(workDir: Path, vararg args: String): DirenvOutput {
private suspend fun run(project: Project, workDir: Path, vararg args: String): DirenvOutput {
val cli = GeneralCommandLine("direnv", *args).withWorkingDirectory(workDir)
val (process, exitCode) = withProgressText("Running ${cli.commandLineString}") {
withContext(Dispatchers.IO) {
mutex.withLock {
project.direnvService.mutex.withLock {
val process = cli.createProcess()
val exitCode = process.awaitExit()
process to exitCode
@ -124,39 +94,17 @@ class DirenvService(val project: Project): SerializablePersistentStateComponent<
return DirenvOutput(stdOut, false)
}
fun hasDotEnv(): Boolean {
if (!isInstalled)
return false
val projectDir = project.guessProjectDir()?.toNioPathOrNull() ?: return false
return envFiles.any { projectDir.resolve(it).isRegularFile() }
}
data class State(
@JvmField
@Attribute
var enabled: DirenvState = DirenvState.Auto
)
companion object {
private const val GROUP_DISPLAY_ID = "zigbrains-direnv"
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)
}
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 direnvInstalled() = _direnvInstalled
}
sealed interface IDirenvService {
val isInstalled: Boolean
val isEnabled: DirenvState
suspend fun import(): Env
}
private val envFiles = listOf(".envrc", ".env")
suspend fun Project?.getDirenv(): Env {
if (this == null)
return emptyEnv
return DirenvCmd.importDirenv(this)
}

View file

@ -22,19 +22,14 @@
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
enum class DirenvState {
Auto,
Enabled,
Disabled;
@Service(Service.Level.PROJECT)
class DirenvProjectService {
val mutex = Mutex()
}
fun isEnabled(project: Project?): Boolean {
return when(this) {
Enabled -> true
Disabled -> false
Auto -> project?.service<DirenvService>()?.hasDotEnv() == true
}
}
}
val Project.direnvService get() = service<DirenvProjectService>()

View file

@ -25,26 +25,20 @@ 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.flow
import kotlinx.coroutines.flow.flowOn
import org.jetbrains.annotations.NonNls
import java.io.File
import kotlin.io.path.absolute
import kotlin.io.path.isDirectory
import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile
import java.nio.file.Path
import kotlin.io.path.*
@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) }
fun findAllExecutablesOnPATH(exe: @NonNls String) = flow {
fun findExecutableOnPATH(exe: @NonNls String): Path? {
val exeName = if (SystemInfo.isWindows) "$exe.exe" else exe
val paths = path ?: return@flow
val paths = path ?: return null
for (dir in paths) {
val path = dir.toNioPathOrNull()?.absolute() ?: continue
if (!path.toFile().exists() || !path.isDirectory())
@ -52,11 +46,10 @@ data class Env(val env: Map<String, String>) {
val exePath = path.resolve(exeName).absolute()
if (!exePath.isRegularFile() || !exePath.isExecutable())
continue
emit(exePath)
return exePath
}
}.flowOn(Dispatchers.IO)
companion object {
val empty = Env(emptyMap())
return null
}
}
val emptyEnv = Env(emptyMap())

View file

@ -1,101 +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.direnv.ui
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.direnv.DirenvState
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider.Companion.PROJECT_KEY
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.ui.dsl.builder.Panel
import java.awt.event.ItemEvent
abstract class DirenvEditor<T>(private val sharedState: ZigProjectConfigurationProvider.IUserDataBridge?): SubConfigurable<T> {
private var cb: ComboBox<DirenvState>? = null
override fun attach(panel: Panel): Unit = with(panel) {
row(ZigBrainsBundle.message("settings.direnv.enable.label")) {
comboBox(DirenvState.entries).component.let {
cb = it
if (sharedState != null) {
it.addItemListener { e ->
if (e.stateChange != ItemEvent.SELECTED)
return@addItemListener
val item = e.item
if (item !is DirenvState)
return@addItemListener
DirenvService.setStateFor(sharedState, item)
}
}
}
}
}
override fun isModified(context: T): Boolean {
return isEnabled(context) != cb?.selectedItem as DirenvState
}
override fun apply(context: T) {
setEnabled(context, cb?.selectedItem as DirenvState)
}
override fun reset(context: T?) {
if (context == null) {
cb?.selectedItem = DirenvState.Auto
return
}
cb?.selectedItem = isEnabled(context)
}
override fun dispose() {
}
abstract fun isEnabled(context: T): DirenvState
abstract fun setEnabled(context: T, value: DirenvState)
class ForProject(sharedState: ZigProjectConfigurationProvider.IUserDataBridge) : DirenvEditor<Project>(sharedState) {
override fun isEnabled(context: Project): DirenvState {
return DirenvService.getInstance(context).isEnabled
}
override fun setEnabled(context: Project, value: DirenvState) {
context.service<DirenvService>().isEnabledRaw = value
}
}
class Provider: ZigProjectConfigurationProvider {
override fun create(sharedState: ZigProjectConfigurationProvider.IUserDataBridge): SubConfigurable<Project>? {
if (sharedState.getUserData(PROJECT_KEY)?.isDefault != false) {
return null
}
DirenvService.setStateFor(sharedState, DirenvState.Auto)
return ForProject(sharedState)
}
override val index: Int
get() = 100
}
}

View file

@ -1,90 +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.actions
import com.falsepattern.zigbrains.project.execution.base.ZigExecConfig
import com.falsepattern.zigbrains.project.execution.run.ZigConfigProducerRun
import com.falsepattern.zigbrains.project.execution.run.ZigExecConfigRun
import com.falsepattern.zigbrains.project.execution.test.ZigConfigProducerTest
import com.falsepattern.zigbrains.project.execution.test.ZigExecConfigTest
import com.intellij.execution.ExecutionManager
import com.intellij.execution.actions.ConfigurationContext
import com.intellij.execution.actions.RunConfigurationProducer
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.runners.ExecutionEnvironmentBuilder
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.project.DumbAwareAction
class ZigRunFileAction: DumbAwareAction() {
override fun actionPerformed(e: AnActionEvent) {
val file = e.getData(CommonDataKeys.PSI_FILE) ?: return
val config = getConfig(e) ?: return
val project = file.project
val builder = ExecutionEnvironmentBuilder.createOrNull(DefaultRunExecutor.getRunExecutorInstance(), config) ?: return
ExecutionManager.getInstance(project).restartRunProfile(builder.build())
}
private fun getConfig(e: AnActionEvent): ZigExecConfig<*>? {
val context = ConfigurationContext.getFromContext(e.dataContext, e.place)
return getRunConfiguration(context) ?: getTestConfiguration(context)
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabledAndVisible = getConfig(e) != null
}
private fun getRunConfiguration(context: ConfigurationContext): ZigExecConfigRun? {
try {
val configProducer = RunConfigurationProducer.getInstance(ZigConfigProducerRun::class.java)
val settings = configProducer.findExistingConfiguration(context)
if (settings != null) {
return settings.configuration as? ZigExecConfigRun
}
val fromContext = configProducer.createConfigurationFromContext(context)
if (fromContext != null) {
return fromContext.configuration as? ZigExecConfigRun
}
} catch (_: NullPointerException) {}
return null
}
private fun getTestConfiguration(context: ConfigurationContext): ZigExecConfigTest? {
try {
val configProducer = RunConfigurationProducer.getInstance(ZigConfigProducerTest::class.java)
val settings = configProducer.findExistingConfiguration(context)
if (settings != null) {
return settings.configuration as? ZigExecConfigTest
}
val fromContext = configProducer.createConfigurationFromContext(context)
if (fromContext != null) {
return fromContext.configuration as? ZigExecConfigTest
}
} catch (_: NullPointerException) {}
return null
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}

View file

@ -24,17 +24,20 @@ 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.ui.TextFieldWithBrowseButton
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull
import com.intellij.ui.components.JBCheckBox
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.ui.dsl.builder.panel
@ -158,10 +161,12 @@ class WorkDirectoryConfigurable(@Transient override val serializedName: String)
}
class WorkDirectoryConfigModule(private val serializedName: String) : PathConfigModule<WorkDirectoryConfigurable>() {
private val field = textFieldWithBrowseButton(
null,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.working-directory"))
).also { Disposer.register(this, it) }
private val field = TextFieldWithBrowseButton(
TextBrowseFolderListener(
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.working-directory"))
),
this
)
override var stringValue by field::text
@ -196,9 +201,9 @@ class FilePathConfigurable(
}
class FilePathConfigModule(private val serializedName: String, @Nls private val label: String) : PathConfigModule<FilePathConfigurable>() {
private val field = textFieldWithBrowseButton(
null,
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(),
private val field = TextFieldWithBrowseButton(
TextBrowseFolderListener(FileChooserDescriptorFactory.createSingleFileDescriptor()),
this
)
override var stringValue by field::text
@ -269,6 +274,18 @@ open class CheckboxConfigurable(
}
}
class ColoredConfigurable(serializedName: String): CheckboxConfigurable(serializedName, ZigBrainsBundle.message("exec.option.label.colored-terminal"), true) {
override fun clone(): ColoredConfigurable {
return super.clone() as ColoredConfigurable
}
}
class DirenvConfigurable(serializedName: String, project: Project): CheckboxConfigurable(serializedName, ZigBrainsBundle.message("exec.option.label.direnv"), project.zigProjectSettings.state.direnv) {
override fun clone(): DirenvConfigurable {
return super.clone() as DirenvConfigurable
}
}
class OptimizationConfigurable(
@Transient private val serializedName: String,
var level: OptimizationLevel = OptimizationLevel.Debug,
@ -330,18 +347,14 @@ class ArgsConfigurable(
@Transient private val serializedName: String,
@Transient @Nls private val guiName: String
) : ZigConfigurable<ArgsConfigurable>, Cloneable {
var args: String = ""
var args: List<String> = emptyList()
override fun readExternal(element: Element) {
args = element.readString(serializedName) ?: element.readStrings(serializedName)?.joinToString(separator = " ") { if (it.contains(' ')) "\"$it\"" else it } ?: ""
}
fun argsSplit(): List<String> {
return translateCommandline(args)
args = element.readStrings(serializedName) ?: return
}
override fun writeExternal(element: Element) {
element.writeString(serializedName, args)
element.writeStrings(serializedName, args)
}
override fun createEditor(): ZigConfigModule<ArgsConfigurable> {
@ -363,12 +376,12 @@ class ArgsConfigurable(
}
override fun apply(configurable: ArgsConfigurable): Boolean {
configurable.args = argsField.text ?: ""
configurable.args = translateCommandline(argsField.text)
return true
}
override fun reset(configurable: ArgsConfigurable) {
argsField.text = configurable.args
argsField.text = configurable.args.joinToString(separator = " ")
}
override fun construct(p: Panel): Unit = with(p) {

View file

@ -41,7 +41,7 @@ abstract class ZigConfigProducer<T: ZigExecConfig<T>>: LazyRunConfigurationProdu
val psiFile = element.containingFile as? ZigFile ?: return false
val theFile = psiFile.virtualFile ?: return false
val filePath = theFile.toNioPathOrNull() ?: return false
return setupConfigurationFromContext(configuration, element, psiFile, filePath, theFile)
return setupConfigurationFromContext(configuration, element, filePath, theFile)
}
override fun isConfigurationFromContext(configuration: T, context: ConfigurationContext): Boolean {
@ -49,7 +49,7 @@ abstract class ZigConfigProducer<T: ZigExecConfig<T>>: LazyRunConfigurationProdu
val psiFile = element.containingFile as? ZigFile ?: return false
val theFile = psiFile.virtualFile ?: return false
val filePath = theFile.toNioPathOrNull() ?: return false
return isConfigurationFromContext(configuration, element, psiFile, filePath, theFile)
return isConfigurationFromContext(configuration, element, filePath, theFile)
}
/*
@ -78,7 +78,7 @@ abstract class ZigConfigProducer<T: ZigExecConfig<T>>: LazyRunConfigurationProdu
}
*/
protected abstract fun setupConfigurationFromContext(configuration: T, element: PsiElement, psiFile: ZigFile, filePath: Path, theFile: VirtualFile): Boolean
protected abstract fun isConfigurationFromContext(configuration: T, element: PsiElement, psiFile: ZigFile, filePath: Path, theFile: VirtualFile): Boolean
protected abstract fun setupConfigurationFromContext(configuration: T, element: PsiElement, filePath: Path, theFile: VirtualFile): Boolean
protected abstract fun isConfigurationFromContext(configuration: T, element: PsiElement, filePath: Path, theFile: VirtualFile): Boolean
abstract override fun shouldReplace(self: ConfigurationFromContext, other: ConfigurationFromContext): Boolean
}

View file

@ -22,7 +22,8 @@
package com.falsepattern.zigbrains.project.execution.base
import com.falsepattern.zigbrains.direnv.DirenvService
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory
@ -41,6 +42,10 @@ import org.jetbrains.annotations.Nls
abstract class ZigExecConfig<T: ZigExecConfig<T>>(project: Project, factory: ConfigurationFactory, @Nls name: String): LocatableConfigurationBase<ZigProfileState<T>>(project, factory, name) {
var workingDirectory = WorkDirectoryConfigurable("workingDirectory").apply { path = project.guessProjectDir()?.toNioPathOrNull() }
private set
var pty = CheckboxConfigurable("pty", ZigBrainsBundle.message("exec.option.label.emulate-terminal"), false)
private set
var direnv = DirenvConfigurable("direnv", project)
private set
abstract val suggestedName: @ActionText String
@Throws(ExecutionException::class)
@ -63,19 +68,24 @@ abstract class ZigExecConfig<T: ZigExecConfig<T>>(project: Project, factory: Con
suspend fun patchCommandLine(commandLine: GeneralCommandLine): GeneralCommandLine {
val direnv = DirenvService.getInstance(project)
if (direnv.isEnabled.isEnabled(project)) {
commandLine.withEnvironment(direnv.import().env)
if (direnv.value) {
commandLine.withEnvironment(DirenvCmd.importDirenv(project).env)
}
return commandLine
}
fun emulateTerminal(): Boolean {
return pty.value
}
override fun clone(): T {
val myClone = super.clone() as ZigExecConfig<*>
myClone.workingDirectory = workingDirectory.clone()
myClone.pty = pty.clone()
myClone.direnv = direnv.clone()
@Suppress("UNCHECKED_CAST")
return myClone as T
}
open fun getConfigurables(): List<ZigConfigurable<*>> = listOf(workingDirectory)
open fun getConfigurables(): List<ZigConfigurable<*>> = listOf(workingDirectory, pty, direnv)
}

View file

@ -23,16 +23,18 @@
package com.falsepattern.zigbrains.project.execution.base
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.ZigConsoleBuilder
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.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking
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.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.platform.ide.progress.ModalTaskOwner
import kotlin.io.path.pathString
@ -42,10 +44,6 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
val configuration: T
): CommandLineState(environment) {
init {
consoleBuilder = ZigConsoleBuilder(environment.project, true)
}
@Throws(ExecutionException::class)
override fun startProcess(): ProcessHandler {
return runModalOrBlocking({ModalTaskOwner.project(environment.project)}, {"ZigProfileState.startProcess"}) {
@ -55,20 +53,37 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
@Throws(ExecutionException::class)
suspend fun startProcessSuspend(): ProcessHandler {
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)
val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain"))
return ZigProcessHandler(getCommandLine(toolchain, false))
}
@Throws(ExecutionException::class)
open suspend fun getCommandLine(toolchain: ZigToolchain, debug: Boolean): GeneralCommandLine {
open suspend fun getCommandLine(toolchain: AbstractZigToolchain, debug: Boolean): GeneralCommandLine {
val workingDir = configuration.workingDirectory
val zigExePath = toolchain.zig.path()
val cli = PtyCommandLine().withConsoleMode(false)
// TODO remove this check once JetBrains implements colored terminal in the debugger
// https://youtrack.jetbrains.com/issue/CPP-11622/ANSI-color-codes-not-honored-in-Debug-Run-Configuration-output-window
val cli = if (configuration.emulateTerminal() && !debug) PtyCommandLine().withConsoleMode(true).withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) else GeneralCommandLine()
cli.withExePath(zigExePath.pathString)
workingDir.path?.let { cli.withWorkingDirectory(it) }
cli.withCharset(Charsets.UTF_8)
cli.addParameters(configuration.buildCommandLineArgs(debug))
return configuration.patchCommandLine(cli)
}
}
@Throws(ExecutionException::class)
fun executeCommandLine(commandLine: GeneralCommandLine, environment: ExecutionEnvironment): DefaultExecutionResult {
val handler = startProcess(commandLine)
val console = BuildTextConsoleView(environment.project, false, emptyList())
console.attachToProcess(handler)
return DefaultExecutionResult(console, handler)
}
@Throws(ExecutionException::class)
fun startProcess(commandLine: GeneralCommandLine): ProcessHandler {
val handler = ZigProcessHandler(commandLine)
ProcessTerminatedListener.attach(handler)
return handler
}

View file

@ -1,56 +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.execution.base
import com.falsepattern.zigbrains.zig.psi.ZigContainerMembers
import com.falsepattern.zigbrains.zig.psi.ZigFile
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.isFile
import com.intellij.psi.util.childrenOfType
fun ZigFile.hasMainFunction(): Boolean {
val members = childrenOfType<ZigContainerMembers>().firstOrNull() ?: return false
return members.containerDeclarationList.any { it.decl?.fnProto?.identifier?.textMatches("main") == true }
}
fun ZigFile.hasTests(): Boolean {
val members = childrenOfType<ZigContainerMembers>().firstOrNull() ?: return false
return members.containerDeclarationList.any { it.testDecl != null }
}
fun VirtualFile.findBuildZig(): VirtualFile? {
var parent = this.parent
while (parent != null) {
parent.children.forEach {
if (it.isFile && it.name == "build.zig") {
return it
}
}
parent = parent.parent
}
return null
}
fun VirtualFile.isBuildZig(): Boolean {
return name == "build.zig"
}

View file

@ -24,16 +24,11 @@ package com.falsepattern.zigbrains.project.execution.build
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.ZigConfigProducer
import com.falsepattern.zigbrains.project.execution.base.findBuildZig
import com.falsepattern.zigbrains.project.execution.base.isBuildZig
import com.falsepattern.zigbrains.project.execution.firstConfigFactory
import com.falsepattern.zigbrains.zig.psi.ZigFile
import com.falsepattern.zigbrains.zig.psi.ZigTypes
import com.intellij.execution.actions.ConfigurationFromContext
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiElement
import com.intellij.psi.util.elementType
import java.nio.file.Path
class ZigConfigProducerBuild: ZigConfigProducer<ZigExecConfigBuild>() {
@ -41,43 +36,21 @@ class ZigConfigProducerBuild: ZigConfigProducer<ZigExecConfigBuild>() {
return firstConfigFactory<ZigConfigTypeBuild>()
}
override fun setupConfigurationFromContext(configuration: ZigExecConfigBuild, element: PsiElement, psiFile: ZigFile, filePath: Path, theFile: VirtualFile): Boolean {
if (theFile.isBuildZig()) {
configuration.name = ZigBrainsBundle.message("configuration.build.marker-run")
configuration.buildSteps.args = "run"
configuration.debugBuildSteps.args = "install"
return true
}
val buildZig = theFile.findBuildZig() ?: return false
configuration.workingDirectory.path = buildZig.parent.toNioPath()
if (element.elementType == ZigTypes.KEYWORD_TEST) {
configuration.name = ZigBrainsBundle.message("configuration.build.marker-test")
configuration.buildSteps.args = "test"
configuration.debugBuildSteps.args = "install_test"
return true
} else {
configuration.name = ZigBrainsBundle.message("configuration.build.marker-run")
configuration.buildSteps.args = "run"
configuration.debugBuildSteps.args = "install"
override fun setupConfigurationFromContext(configuration: ZigExecConfigBuild, element: PsiElement, filePath: Path, theFile: VirtualFile): Boolean {
if (LINE_MARKER.elementMatches(element)) {
configuration.name = ZigBrainsBundle.message("configuration.build.marker-name")
return true
}
return false
}
override fun isConfigurationFromContext(configuration: ZigExecConfigBuild, element: PsiElement, psiFile: ZigFile, filePath: Path, theFile: VirtualFile): Boolean {
val dir = configuration.workingDirectory.path ?: return false
if (theFile.isBuildZig()) {
return filePath.parent == dir
} else {
if (element.elementType == ZigTypes.KEYWORD_TEST) {
if (configuration.buildSteps.args != "test")
return false
}
val buildZig = theFile.findBuildZig() ?: return false
return buildZig.parent.toNioPath() == dir
}
override fun isConfigurationFromContext(configuration: ZigExecConfigBuild, element: PsiElement, filePath: Path, theFile: VirtualFile): Boolean {
return filePath.parent == (configuration.workingDirectory.path ?: return false)
}
override fun shouldReplace(self: ConfigurationFromContext, other: ConfigurationFromContext): Boolean {
return self.configurationType is ZigConfigTypeBuild
}
}
private val LINE_MARKER = ZigLineMarkerBuild()

View file

@ -25,6 +25,7 @@ package com.falsepattern.zigbrains.project.execution.build
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.*
import com.falsepattern.zigbrains.shared.ZBFeatures
import com.falsepattern.zigbrains.shared.cli.coloredCliFlags
import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory
@ -36,9 +37,7 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
private set
var extraArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.build.args"))
private set
var debugBuildSteps = ArgsConfigurable("debugBuildSteps", ZigBrainsBundle.message("exec.option.label.build.steps-debug"))
private set
var debugExtraArgs = ArgsConfigurable("debugCompilerArgs", ZigBrainsBundle.message("exec.option.label.build.args-debug"))
var colored = ColoredConfigurable("colored")
private set
var exePath = FilePathConfigurable("exePath", ZigBrainsBundle.message("exec.option.label.build.exe-path-debug"))
private set
@ -49,9 +48,22 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
val result = ArrayList<String>()
result.add("build")
val steps = if (debug) debugBuildSteps.argsSplit() else buildSteps.argsSplit()
val steps = if (debug) {
val truncatedSteps = ArrayList<String>()
for (step in buildSteps.args) {
if (step == "run")
continue
if (step == "test")
throw ExecutionException(ZigBrainsBundle.message("exception.zig-build.debug.test-not-supported"))
truncatedSteps.add(step)
}
truncatedSteps
} else buildSteps.args
result.addAll(steps)
result.addAll(if (debug) debugExtraArgs.argsSplit() else extraArgs.argsSplit())
result.addAll(coloredCliFlags(colored.value, debug))
result.addAll(extraArgs.args)
return result
}
@ -62,15 +74,16 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
val clone = super.clone()
clone.buildSteps = buildSteps.clone()
clone.exeArgs = exeArgs.clone()
clone.colored = colored.clone()
clone.exePath = exePath.clone()
clone.exeArgs = exeArgs.clone()
return clone
}
override fun getConfigurables(): List<ZigConfigurable<*>> {
val baseCfg = super.getConfigurables() + listOf(buildSteps, extraArgs)
val baseCfg = super.getConfigurables() + listOf(buildSteps, extraArgs, colored)
return if (ZBFeatures.debug()) {
baseCfg + listOf(debugBuildSteps, debugExtraArgs, exePath, exeArgs)
baseCfg + listOf(exePath, exeArgs)
} else {
baseCfg
}

View file

@ -38,7 +38,7 @@ class ZigLineMarkerBuild: ZigTopLevelLineMarker() {
return null
val parent = element.parent ?: return null
if (parent.elementType != ZigTypes.FN_PROTO)
if (parent.elementType != ZigTypes.FN_DECL_PROTO)
return null
val file = element.containingFile ?: return null

View file

@ -23,10 +23,7 @@
package com.falsepattern.zigbrains.project.execution.run
import com.falsepattern.zigbrains.project.execution.base.ZigConfigProducer
import com.falsepattern.zigbrains.project.execution.base.findBuildZig
import com.falsepattern.zigbrains.project.execution.base.hasMainFunction
import com.falsepattern.zigbrains.project.execution.firstConfigFactory
import com.falsepattern.zigbrains.zig.psi.ZigFile
import com.intellij.execution.actions.ConfigurationFromContext
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.openapi.vfs.VirtualFile
@ -38,19 +35,16 @@ class ZigConfigProducerRun: ZigConfigProducer<ZigExecConfigRun>() {
return firstConfigFactory<ZigConfigTypeRun>()
}
override fun setupConfigurationFromContext(configuration: ZigExecConfigRun, element: PsiElement, psiFile: ZigFile, filePath: Path, theFile: VirtualFile): Boolean {
if (!psiFile.hasMainFunction()) {
return false
override fun setupConfigurationFromContext(configuration: ZigExecConfigRun, element: PsiElement, filePath: Path, theFile: VirtualFile): Boolean {
if (LINE_MARKER.elementMatches(element)) {
configuration.filePath.path = filePath
configuration.name = theFile.presentableName
return true
}
if (theFile.findBuildZig() != null) {
return false
}
configuration.filePath.path = filePath
configuration.name = theFile.presentableName
return true
return false
}
override fun isConfigurationFromContext(configuration: ZigExecConfigRun, element: PsiElement, psiFile: ZigFile, filePath: Path, theFile: VirtualFile): Boolean {
override fun isConfigurationFromContext(configuration: ZigExecConfigRun, element: PsiElement, filePath: Path, theFile: VirtualFile): Boolean {
return filePath == configuration.filePath.path
}
@ -58,3 +52,5 @@ class ZigConfigProducerRun: ZigConfigProducer<ZigExecConfigRun>() {
return self.configurationType is ZigConfigTypeRun
}
}
private val LINE_MARKER = ZigLineMarkerRun()

View file

@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.project.execution.run
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.*
import com.falsepattern.zigbrains.shared.cli.coloredCliFlags
import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory
@ -34,6 +35,8 @@ import kotlin.io.path.pathString
class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExecConfig<ZigExecConfigRun>(project, factory, ZigBrainsBundle.message("exec.type.run.label")) {
var filePath = FilePathConfigurable("filePath", ZigBrainsBundle.message("exec.option.label.file-path"))
private set
var colored = ColoredConfigurable("colored")
private set
var optimization = OptimizationConfigurable("optimization")
private set
var compilerArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.compiler-args"))
@ -44,14 +47,15 @@ class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExec
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
val result = ArrayList<String>()
result.add(if (debug) "build-exe" else "run")
result.addAll(coloredCliFlags(colored.value, debug))
result.add(filePath.path?.pathString ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig.empty-file-path")))
if (!debug || optimization.forced) {
result.addAll(listOf("-O", optimization.level.name))
}
result.addAll(compilerArgs.argsSplit())
result.addAll(compilerArgs.args)
if (!debug) {
result.add("--")
result.addAll(exeArgs.argsSplit())
result.addAll(exeArgs.args)
}
return result
}
@ -62,6 +66,7 @@ class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExec
override fun clone(): ZigExecConfigRun {
val clone = super.clone()
clone.filePath = filePath.clone()
clone.colored = colored.clone()
clone.compilerArgs = compilerArgs.clone()
clone.optimization = optimization.clone()
clone.exeArgs = exeArgs.clone()
@ -69,7 +74,7 @@ class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExec
}
override fun getConfigurables(): List<ZigConfigurable<*>> {
return super.getConfigurables() + listOf(filePath, optimization, compilerArgs, exeArgs)
return super.getConfigurables() + listOf(filePath, optimization, colored, compilerArgs, exeArgs)
}
override fun getState(executor: Executor, environment: ExecutionEnvironment): ZigProfileState<ZigExecConfigRun> {

View file

@ -38,7 +38,7 @@ class ZigLineMarkerRun: ZigTopLevelLineMarker() {
return null
val parent = element.parent
if (parent.elementType != ZigTypes.FN_PROTO)
if (parent.elementType != ZigTypes.FN_DECL_PROTO)
return null
return parent.parent

View file

@ -24,10 +24,7 @@ package com.falsepattern.zigbrains.project.execution.test
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.ZigConfigProducer
import com.falsepattern.zigbrains.project.execution.base.findBuildZig
import com.falsepattern.zigbrains.project.execution.base.hasTests
import com.falsepattern.zigbrains.project.execution.firstConfigFactory
import com.falsepattern.zigbrains.zig.psi.ZigFile
import com.intellij.execution.actions.ConfigurationFromContext
import com.intellij.execution.configurations.ConfigurationFactory
import com.intellij.openapi.vfs.VirtualFile
@ -39,23 +36,22 @@ class ZigConfigProducerTest: ZigConfigProducer<ZigExecConfigTest>() {
return firstConfigFactory<ZigConfigTypeTest>()
}
override fun setupConfigurationFromContext(configuration: ZigExecConfigTest, element: PsiElement, psiFile: ZigFile, filePath: Path, theFile: VirtualFile): Boolean {
if (!psiFile.hasTests()) {
return false
override fun setupConfigurationFromContext(configuration: ZigExecConfigTest, element: PsiElement, filePath: Path, theFile: VirtualFile): Boolean {
if (LINE_MARKER.elementMatches(element)) {
configuration.filePath.path = filePath
configuration.name = ZigBrainsBundle.message("configuration.test.marker-name", theFile.presentableName)
return true
}
if (theFile.findBuildZig() != null) {
return false
}
configuration.filePath.path = filePath
configuration.name = ZigBrainsBundle.message("configuration.test.marker-name", theFile.presentableName)
return true
return false
}
override fun isConfigurationFromContext(configuration: ZigExecConfigTest, element: PsiElement, psiFile: ZigFile, filePath: Path, theFile: VirtualFile): Boolean {
override fun isConfigurationFromContext(configuration: ZigExecConfigTest, element: PsiElement, filePath: Path, theFile: VirtualFile): Boolean {
return filePath == configuration.filePath.path
}
override fun shouldReplace(self: ConfigurationFromContext, other: ConfigurationFromContext): Boolean {
return self.configurationType is ZigConfigTypeTest
}
}
}
private val LINE_MARKER = ZigLineMarkerTest()

View file

@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.project.execution.test
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.base.*
import com.falsepattern.zigbrains.shared.cli.coloredCliFlags
import com.intellij.execution.ExecutionException
import com.intellij.execution.Executor
import com.intellij.execution.configurations.ConfigurationFactory
@ -34,6 +35,8 @@ import kotlin.io.path.pathString
class ZigExecConfigTest(project: Project, factory: ConfigurationFactory): ZigExecConfig<ZigExecConfigTest>(project, factory, ZigBrainsBundle.message("exec.type.test.label")) {
var filePath = FilePathConfigurable("filePath", ZigBrainsBundle.message("exec.option.label.file-path"))
private set
var colored = ColoredConfigurable("colored")
private set
var optimization = OptimizationConfigurable("optimization")
private set
var compilerArgs = ArgsConfigurable("compilerArgs", ZigBrainsBundle.message("exec.option.label.compiler-args"))
@ -43,11 +46,12 @@ class ZigExecConfigTest(project: Project, factory: ConfigurationFactory): ZigExe
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
val result = ArrayList<String>()
result.add("test")
result.addAll(coloredCliFlags(colored.value, debug))
result.add(filePath.path?.pathString ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig.empty-file-path")))
if (!debug || optimization.forced) {
result.addAll(listOf("-O", optimization.level.name))
}
result.addAll(compilerArgs.argsSplit())
result.addAll(compilerArgs.args)
if (debug) {
result.add("--test-no-exec")
}
@ -60,13 +64,14 @@ class ZigExecConfigTest(project: Project, factory: ConfigurationFactory): ZigExe
override fun clone(): ZigExecConfigTest {
val clone = super.clone()
clone.filePath = filePath.clone()
clone.colored = colored.clone()
clone.compilerArgs = compilerArgs.clone()
clone.optimization = optimization.clone()
return clone
}
override fun getConfigurables(): List<ZigConfigurable<*>> {
return super.getConfigurables() + listOf(filePath, optimization, compilerArgs)
return super.getConfigurables() + listOf(filePath, optimization, colored, compilerArgs)
}
override fun getState(executor: Executor, environment: ExecutionEnvironment): ZigProfileState<ZigExecConfigTest> {

View file

@ -29,7 +29,6 @@ import com.intellij.ide.util.projectWizard.ModuleBuilder
import com.intellij.ide.util.projectWizard.ModuleWizardStep
import com.intellij.ide.util.projectWizard.WizardContext
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.roots.ModifiableRootModel
import com.intellij.openapi.util.Disposer
@ -50,7 +49,7 @@ class ZigModuleBuilder: ModuleBuilder() {
override fun getCustomOptionsStep(context: WizardContext?, parentDisposable: Disposable?): ModuleWizardStep? {
val step = ZigModuleWizardStep(parentDisposable)
parentDisposable?.let { Disposer.register(it) { step.peer.dispose() } }
parentDisposable?.let { Disposer.register(it, step.peer) }
return step
}
@ -59,20 +58,20 @@ class ZigModuleBuilder: ModuleBuilder() {
val root = contentEntry.file ?: return
val config = configurationData ?: return
config.generateProject(this, rootModel.project, root, forceGitignore)
withEDTContext(ModalityState.defaultModalityState()) {
withEDTContext {
root.refresh(false, true)
}
}
inner class ZigModuleWizardStep(parent: Disposable?): ModuleWizardStep() {
internal val peer = ZigProjectGeneratorPeer(true).also { Disposer.register(parent ?: return@also) {it.dispose()} }
internal val peer = ZigProjectGeneratorPeer(true).also { Disposer.register(parent ?: return@also, it) }
override fun getComponent(): JComponent {
return peer.myComponent.withBorder()
}
override fun disposeUIResources() {
Disposer.dispose(peer.newProjectPanel)
Disposer.dispose(peer)
}
override fun updateDataModel() {

View file

@ -23,7 +23,9 @@
package com.falsepattern.zigbrains.project.newproject
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
import com.falsepattern.zigbrains.project.template.ZigExecutableTemplate
import com.falsepattern.zigbrains.project.template.ZigInitTemplate
import com.falsepattern.zigbrains.project.template.ZigLibraryTemplate
import com.falsepattern.zigbrains.project.template.ZigProjectTemplate
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionToolbarPosition
@ -41,7 +43,7 @@ import javax.swing.ListSelectionModel
class ZigNewProjectPanel(private var handleGit: Boolean): Disposable {
private val git = JBCheckBox()
val panels = ZigProjectConfigurationProvider.createPanels(null).onEach { Disposer.register(this, it) }
private val panels = ZigProjectConfigurationProvider.createNewProjectSettingsPanels().onEach { Disposer.register(this, it) }
private val templateList = JBList(JBList.createDefaultListModel(defaultTemplates)).apply {
selectionMode = ListSelectionModel.SINGLE_SELECTION
selectedIndex = 0
@ -64,7 +66,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable {
fun getData(): ZigProjectConfigurationData {
val selectedTemplate = templateList.selectedValue
return ZigProjectConfigurationData(handleGit && git.isSelected, panels, selectedTemplate)
return ZigProjectConfigurationData(handleGit && git.isSelected, panels.map { it.data }, selectedTemplate)
}
fun attach(p: Panel): Unit = with(p) {
@ -73,7 +75,6 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable {
cell(git)
}
}
panels.filter { it.newProjectBeforeInitSelector }.forEach { it.attach(p) }
group("Zig Project Template") {
row {
resizableRow()
@ -82,7 +83,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable {
.align(AlignY.FILL)
}
}
panels.filter { !it.newProjectBeforeInitSelector }.forEach { it.attach(p) }
panels.forEach { it.attach(p) }
}
override fun dispose() {
@ -91,7 +92,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable {
private val defaultTemplates get() = listOf(
// ZigExecutableTemplate(),
// ZigLibraryTemplate(),
ZigExecutableTemplate(),
ZigLibraryTemplate(),
ZigInitTemplate()
)

View file

@ -22,10 +22,9 @@
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
@ -43,7 +42,7 @@ import kotlinx.coroutines.launch
@JvmRecord
data class ZigProjectConfigurationData(
val git: Boolean,
val conf: List<SubConfigurable<Project>>,
val conf: List<ZigProjectConfigurationProvider.Settings>,
val selectedTemplate: ZigProjectTemplate
) {
@RequiresBackgroundThread
@ -55,7 +54,9 @@ data class ZigProjectConfigurationData(
if (!reporter.indeterminateStep("Initializing project") {
if (template is ZigInitTemplate) {
val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run {
val toolchain = conf
.mapNotNull { it as? ZigProjectConfigurationProvider.ToolchainProvider }
.firstNotNullOfOrNull { it.toolchain } ?: run {
Notification(
"zigbrains",
"Tried to generate project with zig init, but zig toolchain is invalid",
@ -72,20 +73,13 @@ data class ZigProjectConfigurationData(
).notify(project)
return@indeterminateStep false
}
val result = zig.callWithArgs(workDir, "init").getOrElse { throwable ->
Notification(
"zigbrains",
"Failed to run \"zig init\": ${throwable.message}",
NotificationType.ERROR
).notify(project)
return@indeterminateStep false
}
val result = zig.callWithArgs(workDir, "init")
if (result.exitCode != 0) {
Notification(
"zigbrains",
"\"zig init\" failed with exit code ${result.exitCode}! Check the IDE log files!",
NotificationType.ERROR
).notify(project)
)
System.err.println(result.stderr)
return@indeterminateStep false
}
@ -118,10 +112,10 @@ data class ZigProjectConfigurationData(
if (git) {
project.zigCoroutineScope.launch {
GitRepositoryInitializer.getInstance()?.initRepository(project, baseDir)
createGitIgnoreFile(baseDir, requestor)
createGitIgnoreFile(project, baseDir, requestor)
}
} else if (forceGitignore) {
createGitIgnoreFile(baseDir, requestor)
createGitIgnoreFile(project, baseDir, requestor)
}
return@reportProgress true
@ -130,7 +124,7 @@ data class ZigProjectConfigurationData(
}
private suspend fun createGitIgnoreFile(projectDir: VirtualFile, requestor: Any) {
private suspend fun createGitIgnoreFile(project: Project, projectDir: VirtualFile, requestor: Any) {
if (projectDir.findChild(".gitignore") != null) {
return
}

View file

@ -23,15 +23,17 @@
package com.falsepattern.zigbrains.project.newproject
import com.intellij.ide.util.projectWizard.SettingsStep
import com.intellij.openapi.Disposable
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.Disposer
import com.intellij.platform.ProjectGeneratorPeer
import com.intellij.ui.dsl.builder.panel
import javax.swing.JComponent
class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigProjectConfigurationData> {
val newProjectPanel by lazy {
ZigNewProjectPanel(handleGit)
class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigProjectConfigurationData>, Disposable {
private val newProjectPanel by lazy {
ZigNewProjectPanel(handleGit).also { Disposer.register(this, it) }
}
val myComponent: JComponent by lazy {
panel {
@ -59,7 +61,6 @@ class ZigProjectGeneratorPeer(var handleGit: Boolean): ProjectGeneratorPeer<ZigP
return false
}
fun dispose() {
newProjectPanel.dispose()
override fun dispose() {
}
}

View file

@ -24,29 +24,73 @@ package com.falsepattern.zigbrains.project.run
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.AnsiEscapeDecoder.ColoredTextAcceptor
import com.intellij.execution.process.KillableColoredProcessHandler
import com.intellij.openapi.util.Key
import com.pty4j.PtyProcess
import java.nio.charset.Charset
open class ZigProcessHandler : KillableProcessHandler {
class ZigProcessHandler : KillableColoredProcessHandler, ColoredTextAcceptor {
constructor(commandLine: GeneralCommandLine) : super(commandLine) {
setHasPty(commandLine is PtyCommandLine)
setShouldDestroyProcessRecursively(!hasPty())
}
protected constructor (process: Process, commandLine: String, charset: Charset) : super(process, commandLine, charset) {
constructor (process: Process, commandLine: String, charset: Charset) : super(process, commandLine, charset) {
setHasPty(process is PtyProcess)
setShouldDestroyProcessRecursively(!hasPty())
}
class IPCAware : ZigProcessHandler {
val originalCommandLine: String
constructor(originalCommandLine: String, commandLine: GeneralCommandLine) : super(commandLine) {
this.originalCommandLine = originalCommandLine
}
fun unwrap(): ZigProcessHandler {
return ZigProcessHandler(this.process, this.originalCommandLine, this.charset ?: Charsets.UTF_8)
}
override fun coloredTextAvailable(text: String, attributes: Key<*>) {
super.coloredTextAvailable(text.translateVT100Escapes(), attributes)
}
}
}
private val VT100_CHARS = CharArray(256).apply {
this.fill(' ')
this[0x6A] = '┘'
this[0x6B] = '┐'
this[0x6C] = '┌'
this[0x6D] = '└'
this[0x6E] = '┼'
this[0x71] = '─'
this[0x74] = '├'
this[0x75] = '┤'
this[0x76] = '┴'
this[0x77] = '┬'
this[0x78] = '│'
}
private const val VT100_BEGIN_SEQ = "\u001B(0"
private const val VT100_END_SEQ = "\u001B(B"
private const val VT100_BEGIN_SEQ_LENGTH: Int = VT100_BEGIN_SEQ.length
private const val VT100_END_SEQ_LENGTH: Int = VT100_END_SEQ.length
private fun String.translateVT100Escapes(): String {
var offset = 0
val result = StringBuilder()
val textLength = length
while (offset < textLength) {
val startIndex = indexOf(VT100_BEGIN_SEQ, offset)
if (startIndex < 0) {
result.append(substring(offset, textLength).replace(VT100_END_SEQ, ""))
break
}
result.append(this, offset, startIndex)
val blockOffset = startIndex + VT100_BEGIN_SEQ_LENGTH
var endIndex = indexOf(VT100_END_SEQ, blockOffset)
if (endIndex < 0) {
endIndex = textLength
}
for (i in blockOffset until endIndex) {
val c = this[i].code
if (c >= 256) {
result.append(c)
} else {
result.append(VT100_CHARS[c])
}
}
offset = endIndex + VT100_END_SEQ_LENGTH
}
return result.toString()
}

View file

@ -23,8 +23,8 @@
package com.falsepattern.zigbrains.project.run
import com.falsepattern.zigbrains.project.execution.base.ZigProfileState
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainService
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
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 = ZigToolchainService.getInstance(environment.project).toolchain ?: run {
val toolchain = environment.project.zigProjectSettings.state.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: ZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor?
abstract suspend fun execute(state: ProfileState, toolchain: AbstractZigToolchain, environment: ExecutionEnvironment): RunContentDescriptor?
}

View file

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

View file

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

View file

@ -20,18 +20,23 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.project.execution
package com.falsepattern.zigbrains.project.settings
import com.intellij.execution.filters.TextConsoleBuilderImpl
import com.intellij.execution.ui.ConsoleView
import com.falsepattern.zigbrains.shared.SubConfigurable
import com.intellij.openapi.project.Project
import com.intellij.terminal.TerminalExecutionConsole
class ZigConsoleBuilder(private val project: Project, private val emulateTerminal: Boolean = false): TextConsoleBuilderImpl(project) {
override fun createConsole(): ConsoleView {
return if (emulateTerminal)
TerminalExecutionConsole(project, null)
else
super.createConsole()
class ZigCoreProjectConfigurationProvider: ZigProjectConfigurationProvider {
override fun handleMainConfigChanged(project: Project) {
}
override fun createConfigurable(project: Project): SubConfigurable {
return ZigProjectConfigurable(project)
}
override fun createNewProjectSettingsPanel(): ZigProjectConfigurationProvider.SettingsPanel {
return ZigProjectSettingsPanel(null)
}
override val priority: Int
get() = 0
}

View file

@ -0,0 +1,58 @@
/*
* 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(panel: Panel) {
settingsPanel?.let { Disposer.dispose(it) }
settingsPanel = ZigProjectSettingsPanel(project).apply { attach(panel) }.also { Disposer.register(this, it) }
}
override fun isModified(): Boolean {
return project.zigProjectSettings.isModified(settingsPanel?.data ?: return false)
}
override fun apply() {
val service = project.zigProjectSettings
val data = settingsPanel?.data ?: return
val modified = service.isModified(data)
service.state = data
if (modified) {
ZigProjectConfigurationProvider.mainConfigChanged(project)
}
}
override fun reset() {
settingsPanel?.data = project.zigProjectSettings.state
}
override fun dispose() {
settingsPanel = null
}
}

View file

@ -22,56 +22,38 @@
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.openapi.util.Key
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.ui.dsl.builder.Panel
interface ZigProjectConfigurationProvider {
fun create(sharedState: IUserDataBridge): SubConfigurable<Project>?
val index: Int
fun handleMainConfigChanged(project: Project)
fun createConfigurable(project: Project): SubConfigurable
fun createNewProjectSettingsPanel(): SettingsPanel
val priority: Int
companion object {
private val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigProjectConfigurationProvider>("com.falsepattern.zigbrains.projectConfigProvider")
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) }
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(): List<SettingsPanel> {
return EXTENSION_POINT_NAME.extensionList.sortedBy { it.priority }.map { it.createNewProjectSettingsPanel() }
}
}
interface IUserDataBridge: UserDataHolder {
fun addUserDataChangeListener(listener: UserDataListener)
fun removeUserDataChangeListener(listener: UserDataListener)
interface SettingsPanel: Disposable {
val data: Settings
fun attach(p: Panel)
}
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?
}
}
}

View file

@ -0,0 +1,48 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.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.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() = toolchainPath?.toNioPathOrNull()?.let { LocalZigToolchain(it) }
set(value) {
toolchainPath = value?.location?.pathString
}
}

View file

@ -0,0 +1,182 @@
/*
* 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.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.shared.zigCoroutineScope
import com.intellij.openapi.Disposable
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
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
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.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 ZigProjectSettingsPanel(private val project: Project?) : ZigProjectConfigurationProvider.SettingsPanel {
private val direnv = JBCheckBox(ZigBrainsBundle.message("settings.project.label.direnv")).apply { addActionListener {
dispatchAutodetect(true)
} }
private val pathToToolchain = textFieldWithBrowseButton(
project,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
).also {
it.textField.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
dispatchUpdateUI()
}
})
Disposer.register(this, it)
}
private val toolchainVersion = JBLabel()
private val stdFieldOverride = JBCheckBox(ZigBrainsBundle.message("settings.project.label.override-std")).apply {
addChangeListener {
if (isSelected) {
pathToStd.isEnabled = true
} else {
pathToStd.isEnabled = false
updateUI()
}
}
}
private val pathToStd = textFieldWithBrowseButton(
project,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-std"))
).also { Disposer.register(this, it) }
private var debounce: Job? = null
private fun dispatchAutodetect(force: Boolean) {
project.zigCoroutineScope.launchWithEDT {
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, DirenvCmd.direnvInstalled() && project?.isDefault == false && direnv.isSelected)
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) {
val project = project ?: ProjectManager.getInstance().defaultProject
data = project.zigProjectSettings.state
group(ZigBrainsBundle.message("settings.project.group.title")) {
row(ZigBrainsBundle.message("settings.project.label.toolchain")) {
cell(pathToToolchain).resizableColumn().align(AlignX.FILL)
if (DirenvCmd.direnvInstalled() && !project.isDefault) {
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)
}
}
dispatchAutodetect(false)
}
private fun dispatchUpdateUI() {
debounce?.cancel("New debounce")
debounce = project.zigCoroutineScope.launch {
updateUI()
}
}
private suspend fun updateUI() {
val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull()
delay(200)
val toolchain = pathToToolchain?.let { LocalZigToolchain(it) }
val zig = toolchain?.zig
if (zig?.path()?.toFile()?.exists() != true) {
toolchainVersion.text = ""
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
}
return
}
val env = zig.getEnv(project)
val version = env.version
val stdPath = env.stdPath(toolchain, project)
toolchainVersion.text = version
toolchainVersion.foreground = JBColor.foreground()
if (!stdFieldOverride.isSelected) {
pathToStd.text = stdPath?.pathString ?: ""
}
}
override fun dispose() {
debounce?.cancel("Disposed")
}
}

View file

@ -20,26 +20,35 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.lsp.zls
package com.falsepattern.zigbrains.project.settings
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.components.*
import com.intellij.openapi.project.Project
import java.util.*
fun <T: ZigToolchain> T.withZLS(uuid: UUID?): T {
return withExtraData("zls_uuid", uuid?.asString())
@Service(Service.Level.PROJECT)
@State(
name = "ZigProjectSettings",
storages = [Storage("zigbrains.xml")]
)
class ZigProjectSettingsService: PersistentStateComponent<ZigProjectSettings> {
@Volatile
private var state = ZigProjectSettings()
override fun getState(): ZigProjectSettings {
return state.copy()
}
fun setState(value: ZigProjectSettings) {
this.state = value
}
override fun loadState(state: ZigProjectSettings) {
this.state = state
}
fun isModified(otherData: ZigProjectSettings): Boolean {
return state != otherData
}
}
val ZigToolchain.zlsUUID: UUID? get() {
return extraData["zls_uuid"]?.asUUID()
}
val ZigToolchain.zls: ZLSVersion? get() {
return zlsUUID?.let { zlsInstallations[it] }
}
val Project.zls: ZLSVersion? get() = ZigToolchainService.getInstance(this).toolchain?.zls
val Project.zigProjectSettings get() = service<ZigProjectSettingsService>()

View file

@ -1,190 +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.stdlib
import com.falsepattern.zigbrains.Icons
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.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 toolchain: ZigToolchain? = ZigToolchainService.getInstance(project).toolchain
private val roots by lazy {
runBlocking {getRoot(toolchain, project)}?.let { setOf(it) } ?: emptySet()
}
private val name by lazy {
getName(toolchain, project)
}
override fun equals(other: Any?): Boolean {
if (other !is ZigSyntheticLibrary)
return false
return toolchain == other.toolchain
}
override fun hashCode(): Int {
return Objects.hash(roots)
}
override fun getPresentableText(): String {
return name
}
override fun getIcon(unused: Boolean): Icon {
return Icons.Zig
}
override fun getSourceRoots(): Collection<VirtualFile> {
return roots
}
override fun isShowInExternalLibrariesNode(): Boolean {
return !roots.isEmpty()
}
companion object {
private const val ZIG_LIBRARY_ID = "Zig SDK"
private const val ZIG_MODULE_ID = "ZigBrains"
private val libraryTableId = LibraryTableId.ProjectLibraryTableId
private val libraryId = LibraryId(ZIG_LIBRARY_ID, libraryTableId)
private val moduleId = ModuleId(ZIG_MODULE_ID)
suspend fun reload(project: Project, toolchain: ZigToolchain?) {
val root = getRoot(toolchain, project)
if (root != null) {
add(project, root)
} else {
remove(project)
}
}
private suspend fun remove(project: Project) {
val workspaceModel = WorkspaceModel.getInstance(project)
workspaceModel.update("Update Zig std") { builder ->
builder.resolve(moduleId)?.let { moduleEntity ->
builder.removeEntity(moduleEntity)
}
builder.resolve(libraryId)?.let { libraryEntity ->
builder.removeEntity(libraryEntity)
}
}
}
private suspend fun add(project: Project, root: VirtualFile) {
val workspaceModel = WorkspaceModel.getInstance(project)
val libRoot = LibraryRoot(root.toVirtualFileUrl(workspaceModel.getVirtualFileUrlManager()), LibraryRootTypeId.SOURCES)
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)
}
val moduleEntitySource = LegacyBridgeJpsEntitySourceFactory.getInstance(project)
.createEntitySourceForModule(baseModuleDir, null)
val moduleEntity = builder.addEntity(ModuleEntity(ZIG_MODULE_ID, emptyList(), moduleEntitySource))
builder.resolve(libraryId)?.let { libraryEntity ->
builder.removeEntity(libraryEntity)
}
val libraryEntitySource = LegacyBridgeJpsEntitySourceFactory
.getInstance(project)
.createEntitySourceForProjectLibrary(null)
val libraryEntity = LibraryEntity(
ZIG_LIBRARY_ID,
libraryTableId, emptyList(),
libraryEntitySource
) {
roots.add(libRoot)
}
builder.addEntity(libraryEntity)
builder.modifyModuleEntity(moduleEntity) {
val dep = LibraryDependency(libraryId, false, DependencyScope.COMPILE)
dependencies.clear()
dependencies.add(dep)
}
}
}
}
}
private fun getName(
toolchain: ZigToolchain?,
project: Project
): String {
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(
toolchain: ZigToolchain?,
project: Project
): VirtualFile? {
//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
}
val stdPath = toolchain.location.resolve(ePath)
if (stdPath.isAbsolute) {
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run
return roots
}
}
val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null
return roots
}

View file

@ -30,7 +30,6 @@ interface ZigStepDiscoveryListener {
enum class ErrorType {
MissingToolchain,
MissingZigExe,
MissingBuildZig,
GeneralError
}

View file

@ -22,12 +22,11 @@
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
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.Logger
@ -76,7 +75,7 @@ class ZigStepDiscoveryService(private val project: Project) {
private tailrec suspend fun doReload() {
preReload()
val toolchain = ZigToolchainService.getInstance(project).toolchain ?: run {
val toolchain = project.zigProjectSettings.state.toolchain ?: run {
errorReload(ErrorType.MissingToolchain)
return
}
@ -84,15 +83,9 @@ class ZigStepDiscoveryService(private val project: Project) {
val result = zig.callWithArgs(
project.guessProjectDir()?.toNioPathOrNull(),
"build", "-l",
timeoutMillis = currentTimeoutSec * 1000L,
ipcProject = project
).getOrElse { throwable ->
errorReload(ErrorType.MissingZigExe, throwable.message)
null
}
if (result == null) {
} else if (result.checkSuccess(LOG)) {
timeoutMillis = currentTimeoutSec * 1000L
)
if (result.checkSuccess(LOG)) {
currentTimeoutSec = DEFAULT_TIMEOUT_SEC
val lines = result.stdoutLines
val steps = ArrayList<Pair<String, String?>>()
@ -108,7 +101,7 @@ class ZigStepDiscoveryService(private val project: Project) {
} else if (result.isTimeout) {
timeoutReload(currentTimeoutSec)
currentTimeoutSec *= 2
} else if (result.stderrLines.any { it.contains("error: no build.zig file found") }) {
} else if (result.stderrLines.any { it.contains("error: no build.zig file found, in the current directory or any parent directories") }) {
errorReload(ErrorType.MissingBuildZig, result.stderr)
} else {
errorReload(ErrorType.GeneralError, result.stderr)
@ -125,7 +118,7 @@ class ZigStepDiscoveryService(private val project: Project) {
}
private suspend fun dispatchReload() {
withEDTContext(ModalityState.defaultModalityState()) {
withEDTContext {
FileDocumentManager.getInstance().saveAllDocuments()
}
doReload()
@ -160,6 +153,6 @@ val Project.zigStepDiscovery get() = service<ZigStepDiscoveryService>()
private val SPACES = Regex("\\s+")
private const val DEFAULT_TIMEOUT_SEC = 32
private const val DEFAULT_TIMEOUT_SEC = 10
private val LOG = Logger.getInstance(ZigStepDiscoveryService::class.java)

View file

@ -28,7 +28,7 @@ import com.intellij.openapi.project.Project
import com.intellij.ui.SimpleTextAttributes
import javax.swing.Icon
open class BaseNodeDescriptor<T>(project: Project?, displayName: String, displayIcon: Icon? = null, private var description: String? = null): PresentableNodeDescriptor<T>(project, null) {
open class BaseNodeDescriptor<T>(project: Project?, displayName: String, displayIcon: Icon, private var description: String? = null): PresentableNodeDescriptor<T>(project, null) {
init {
icon = displayIcon
myName = displayName

View file

@ -22,6 +22,7 @@
package com.falsepattern.zigbrains.project.steps.ui
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.build.ZigConfigTypeBuild
import com.falsepattern.zigbrains.project.execution.build.ZigExecConfigBuild
@ -29,10 +30,6 @@ import com.falsepattern.zigbrains.project.execution.firstConfigFactory
import com.falsepattern.zigbrains.project.steps.discovery.ZigStepDiscoveryListener
import com.falsepattern.zigbrains.project.steps.discovery.zigStepDiscovery
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ZigIPCService
import com.falsepattern.zigbrains.shared.ipc.ipc
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.execution.ProgramRunnerUtil
import com.intellij.execution.RunManager
import com.intellij.execution.RunnerAndConfigurationSettings
@ -41,58 +38,47 @@ import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.ui.content.Content
import com.intellij.ui.content.ContentFactory
import com.intellij.ui.treeStructure.Tree
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import java.awt.Component
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.BoxLayout
import javax.swing.JPanel
import javax.swing.SwingConstants
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode
import javax.swing.tree.TreePath
@OptIn(ExperimentalUnsignedTypes::class)
class BuildToolWindowContext(private val project: Project): Disposable {
inner class TreeBox() {
val panel = JPanel(VerticalLayout(0))
val root = DefaultMutableTreeNode(BaseNodeDescriptor<Any>(project, ""))
val model = DefaultTreeModel(root)
val tree = Tree(model).also { it.isRootVisible = false }
}
private val viewPanel = JPanel(VerticalLayout(0))
private val stepsBox = TreeBox()
private val buildBox = if (IPCUtil.haveIPC) TreeBox() else null
private var live = AtomicBoolean(true)
val rootNode: DefaultMutableTreeNode = DefaultMutableTreeNode(BaseNodeDescriptor<Any>(project, project.name, AllIcons.Actions.ProjectDirectory))
private val buildZig: DefaultMutableTreeNode = DefaultMutableTreeNode(BaseNodeDescriptor<Any>(project, ZigBrainsBundle.message("build.tool.window.tree.steps.label"), Icons.Zig))
init {
viewPanel.add(JBLabel(ZigBrainsBundle.message("build.tool.window.tree.steps.label")))
viewPanel.add(stepsBox.panel)
stepsBox.panel.setNotScanned()
rootNode.add(buildZig)
}
stepsBox.tree.addMouseListener(object : MouseAdapter() {
private fun setViewportTree(viewport: JBScrollPane) {
val model = DefaultTreeModel(rootNode)
val tree = Tree(model)
tree.expandPath(TreePath(model.getPathToRoot(buildZig)))
viewport.setViewportView(tree)
tree.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (e.clickCount == 2) {
val node = stepsBox.tree.lastSelectedPathComponent as? DefaultMutableTreeNode ?: return
val node = tree.lastSelectedPathComponent as? DefaultMutableTreeNode ?: return
val step = node.userObject as? StepNodeDescriptor ?: return
val stepName = step.element?.name ?: return
@ -101,7 +87,7 @@ class BuildToolWindowContext(private val project: Project): Disposable {
val factory = firstConfigFactory<ZigConfigTypeBuild>()
val newConfig = manager.createConfiguration("zig build $stepName", factory)
val config = newConfig.configuration as ZigExecConfigBuild
config.buildSteps.args = stepName
config.buildSteps.args = listOf(stepName)
manager.addConfiguration(newConfig)
return@run newConfig
}
@ -111,58 +97,6 @@ class BuildToolWindowContext(private val project: Project): Disposable {
}
}
})
if (buildBox != null) {
viewPanel.add(JBLabel(ZigBrainsBundle.message("build.tool.window.tree.build.label")))
viewPanel.add(buildBox.panel)
buildBox.panel.setNoBuilds()
project.zigCoroutineScope.launch {
while (!project.isDisposed && live.get()) {
val ipc = project.ipc ?: return@launch
withTimeoutOrNull(1000) {
ipc.changed.receive()
} ?: continue
ipc.mutex.withLock {
withEDTContext(ModalityState.any()) {
if (ipc.nodes.isEmpty()) {
buildBox.root.removeAllChildren()
buildBox.panel.setNoBuilds()
return@withEDTContext
}
val allNodes = ArrayList(ipc.nodes)
val existingNodes = ArrayList<ZigIPCService.IPCTreeNode>()
val removedNodes = ArrayList<ZigIPCService.IPCTreeNode>()
buildBox.root.children().iterator().forEach { child ->
if (child !is ZigIPCService.IPCTreeNode) {
return@forEach
}
if (child !in allNodes) {
removedNodes.add(child)
} else {
existingNodes.add(child)
}
}
val newNodes = ArrayList<MutableTreeNode>(allNodes)
newNodes.removeAll(existingNodes)
removedNodes.forEach { buildBox.root.remove(it) }
newNodes.forEach { buildBox.root.add(it) }
if (removedNodes.isNotEmpty() || newNodes.isNotEmpty()) {
buildBox.model.reload(buildBox.root)
}
if (buildBox.root.childCount == 0) {
buildBox.panel.setNoBuilds()
} else {
buildBox.panel.setViewportBody(buildBox.tree)
}
for (bn in allNodes) {
expandRecursively(buildBox, bn)
}
}
}
}
}
}
}
private fun createContentPanel(): Content {
@ -186,64 +120,52 @@ class BuildToolWindowContext(private val project: Project): Disposable {
c.weighty = 1.0
c.fill = GridBagConstraints.BOTH
val viewport = JBScrollPane()
viewport.setViewportView(viewPanel)
viewport.setViewportNoContent()
body.add(viewport, c)
val content = ContentFactory.getInstance().createContent(contentPanel, "", false)
content.putUserData(VIEWPORT, viewport)
return content
}
override fun dispose() {
live.set(false)
}
companion object {
suspend fun create(project: Project, window: ToolWindow) {
withEDTContext(ModalityState.any()) {
withEDTContext {
val context = BuildToolWindowContext(project)
Disposer.register(context, project.zigStepDiscovery.register(context.BuildReloadListener()))
Disposer.register(window.disposable, context)
window.contentManager.addContent(context.createContentPanel())
}
}
private fun expandRecursively(box: TreeBox, node: ZigIPCService.IPCTreeNode) {
if (node.changed) {
box.model.reload(node)
node.changed = false
}
box.tree.expandPath(TreePath(box.model.getPathToRoot(node)))
node.children().asIterator().forEach { child ->
(child as? ZigIPCService.IPCTreeNode)?.let { expandRecursively(box, it) }
}
}
}
inner class BuildReloadListener: ZigStepDiscoveryListener {
override suspend fun preReload() {
stepsBox.panel.setRunningZigBuild()
getViewport(project)?.setViewportLoading()
}
override suspend fun postReload(steps: List<Pair<String, String?>>) {
stepsBox.root.removeAllChildren()
buildZig.removeAllChildren()
for ((task, description) in steps) {
val icon = when(task) {
"install" -> AllIcons.Actions.Install
"uninstall" -> AllIcons.Actions.Uninstall
else -> AllIcons.RunConfigurations.TestState.Run
}
stepsBox.root.add(DefaultMutableTreeNode(StepNodeDescriptor(project, task, icon, description)))
buildZig.add(DefaultMutableTreeNode(StepNodeDescriptor(project, task, icon, description)))
}
withEDTContext(ModalityState.any()) {
stepsBox.model.reload(stepsBox.root)
stepsBox.panel.setViewportBody(stepsBox.tree)
withEDTContext {
getViewport(project)?.let { setViewportTree(it) }
}
}
override suspend fun errorReload(type: ZigStepDiscoveryListener.ErrorType, details: String?) {
withEDTContext(ModalityState.any()) {
stepsBox.panel.setViewportError(ZigBrainsBundle.message(when(type) {
withEDTContext {
getViewport(project)?.setViewportError(ZigBrainsBundle.message(when(type) {
ZigStepDiscoveryListener.ErrorType.MissingToolchain -> "build.tool.window.status.error.missing-toolchain"
ZigStepDiscoveryListener.ErrorType.MissingZigExe -> "build.tool.window.status.error.missing-zig-exe"
ZigStepDiscoveryListener.ErrorType.MissingBuildZig -> "build.tool.window.status.error.missing-build-zig"
ZigStepDiscoveryListener.ErrorType.GeneralError -> "build.tool.window.status.error.general"
}), details)
@ -251,32 +173,22 @@ class BuildToolWindowContext(private val project: Project): Disposable {
}
override suspend fun timeoutReload(seconds: Int) {
withEDTContext(ModalityState.any()) {
stepsBox.panel.setViewportError(ZigBrainsBundle.message("build.tool.window.status.timeout", seconds), null)
withEDTContext {
getViewport(project)?.setViewportError(ZigBrainsBundle.message("build.tool.window.status.timeout", seconds), null)
}
}
}
}
private fun JPanel.setViewportBody(component: Component) {
removeAll()
add(component)
repaint()
private fun JBScrollPane.setViewportLoading() {
setViewportView(JBLabel(ZigBrainsBundle.message("build.tool.window.status.loading"), AnimatedIcon.Default(), SwingConstants.CENTER))
}
private fun JPanel.setRunningZigBuild() {
setViewportBody(JBLabel(ZigBrainsBundle.message("build.tool.window.status.loading"), AnimatedIcon.Default(), SwingConstants.CENTER))
private fun JBScrollPane.setViewportNoContent() {
setViewportView(JBLabel(ZigBrainsBundle.message("build.tool.window.status.not-scanned"), AllIcons.General.Information, SwingConstants.CENTER))
}
private fun JPanel.setNotScanned() {
setViewportBody(JBLabel(ZigBrainsBundle.message("build.tool.window.status.not-scanned"), AllIcons.General.Information, SwingConstants.CENTER))
}
private fun JPanel.setNoBuilds() {
setViewportBody(JBLabel(ZigBrainsBundle.message("build.tool.window.status.no-builds"), AllIcons.General.Information, SwingConstants.CENTER))
}
private fun JPanel.setViewportError(msg: String, details: String?) {
private fun JBScrollPane.setViewportError(msg: String, details: String?) {
val result = JPanel()
result.layout = BoxLayout(result, BoxLayout.Y_AXIS)
result.add(JBLabel(msg, AllIcons.General.Error, SwingConstants.CENTER))
@ -287,13 +199,20 @@ private fun JPanel.setViewportError(msg: String, details: String?) {
val scroll = JBScrollPane(code)
result.add(scroll)
}
setViewportBody(result)
setViewportView(result)
}
private fun getViewport(project: Project): JBScrollPane? {
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow("zigbrains.build") ?: return null
val cm = toolWindow.contentManager
val content = cm.getContent(0) ?: return null
return content.getUserData(VIEWPORT)
}
private fun getExistingRunConfig(manager: RunManager, stepName: String): RunnerAndConfigurationSettings? {
for (config in manager.getConfigurationSettingsList(ZigConfigTypeBuild::class.java)) {
val build = config.configuration as? ZigExecConfigBuild ?: continue
val steps = build.buildSteps.argsSplit()
val steps = build.buildSteps.args
if (steps.size != 1)
continue
if (steps[0] != stepName)
@ -302,3 +221,5 @@ private fun getExistingRunConfig(manager: RunManager, stepName: String): RunnerA
}
return null
}
private val VIEWPORT = Key.create<JBScrollPane>("MODEL")

View file

@ -23,13 +23,12 @@
package com.falsepattern.zigbrains.project.steps.ui
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowFactory
import kotlinx.coroutines.launch
class BuildToolWindowFactory: ToolWindowFactory, DumbAware {
class BuildToolWindowFactory: ToolWindowFactory {
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
project.zigCoroutineScope.launch {
BuildToolWindowContext.create(project, toolWindow)

View file

@ -20,19 +20,19 @@
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.lsp.zls.ui
package com.falsepattern.zigbrains.project.toolchain
import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.zls.ZLSVersion
import com.falsepattern.zigbrains.shared.ui.UUIDMapEditor
import com.intellij.openapi.util.NlsContexts
import com.falsepattern.zigbrains.project.toolchain.tools.ZigCompilerTool
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.project.Project
import java.nio.file.Path
class ZLSListEditor : UUIDMapEditor<ZLSVersion>(ZLSDriver.ForList) {
override fun getEmptySelectionString(): String {
return ZLSBundle.message("settings.list.empty")
}
abstract class AbstractZigToolchain {
val zig: ZigCompilerTool by lazy { ZigCompilerTool(this) }
override fun getDisplayName(): @NlsContexts.ConfigurableName String? {
return ZLSBundle.message("settings.list.title")
}
abstract fun workingDirectory(project: Project? = null): Path?
abstract suspend fun patchCommandLine(commandLine: GeneralCommandLine, project: Project? = null): GeneralCommandLine
abstract fun pathToExecutable(toolName: String, project: Project? = null): Path
}

View file

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

View file

@ -0,0 +1,66 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.project.toolchain
import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.io.toNioPathOrNull
import kotlinx.serialization.json.*
import kotlin.io.path.pathString
class LocalZigToolchainProvider: ZigToolchainProvider<LocalZigToolchain> {
override suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): LocalZigToolchain? {
val env = if (project != null && (extraData.getUserData(LocalZigToolchain.DIRENV_KEY) ?: project.zigProjectSettings.state.direnv)) {
DirenvCmd.importDirenv(project)
} else {
emptyEnv
}
val zigExePath = env.findExecutableOnPATH("zig") ?: return null
return LocalZigToolchain(zigExePath.parent)
}
override val serialMarker: String
get() = "local"
override fun deserialize(data: JsonElement): LocalZigToolchain? {
if (data !is JsonObject)
return null
val loc = data["location"] as? JsonPrimitive ?: return null
val path = loc.content.toNioPathOrNull() ?: return null
return LocalZigToolchain(path)
}
override fun canSerialize(toolchain: AbstractZigToolchain): Boolean {
return toolchain is LocalZigToolchain
}
override fun serialize(toolchain: LocalZigToolchain): JsonElement {
return buildJsonObject {
put("location", toolchain.location.pathString)
}
}
}

View file

@ -19,16 +19,15 @@
* 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(
@ -50,4 +49,4 @@ data class ZigToolchainEnvironmentSerializable(
return null
}
}
}

View file

@ -1,57 +0,0 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.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
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
@Service(Service.Level.APP)
@State(
name = "ZigToolchainList",
storages = [Storage("zigbrains.xml")]
)
class ZigToolchainListService: UUIDMapSerializable.Converting<ZigToolchain, ZigToolchain.Ref, MyState>(MyState()) {
override fun serialize(value: ZigToolchain) = value.toRef()
override fun deserialize(value: ZigToolchain.Ref) = value.resolve()
override fun getStorage(state: MyState) = state.toolchains
override fun updateStorage(state: MyState, storage: ToolchainStorage) = state.copy(toolchains = storage)
data class MyState(@JvmField val toolchains: ToolchainStorage = emptyMap())
companion object {
@JvmStatic
fun getInstance(): ZigToolchainListService = service<ZigToolchainListService>()
}
}
inline val zigToolchainList: ZigToolchainListService get() = ZigToolchainListService.getInstance()
private typealias ToolchainStorage = UUIDStorage<ZigToolchain.Ref>

View file

@ -0,0 +1,69 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.project.toolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider.Companion.EXTENSION_POINT_NAME
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.UserDataHolder
import com.intellij.util.xmlb.Converter
import kotlinx.serialization.json.*
sealed interface ZigToolchainProvider<in T: AbstractZigToolchain> {
suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain?
val serialMarker: String
fun deserialize(data: JsonElement): AbstractZigToolchain?
fun canSerialize(toolchain: AbstractZigToolchain): Boolean
fun serialize(toolchain: T): JsonElement
companion object {
val EXTENSION_POINT_NAME = ExtensionPointName.create<ZigToolchainProvider<*>>("com.falsepattern.zigbrains.toolchainProvider")
suspend fun suggestToolchain(project: Project?, extraData: UserDataHolder): AbstractZigToolchain? {
return EXTENSION_POINT_NAME.extensionList.firstNotNullOfOrNull { it.suggestToolchain(project, extraData) }
}
}
}
@Suppress("UNCHECKED_CAST")
private fun <T: AbstractZigToolchain> ZigToolchainProvider<T>.serialize(toolchain: AbstractZigToolchain) = serialize(toolchain as T)
class ZigToolchainConverter: Converter<AbstractZigToolchain>() {
override fun fromString(value: String): AbstractZigToolchain? {
val json = Json.parseToJsonElement(value) as? JsonObject ?: return null
val marker = (json["marker"] as? JsonPrimitive)?.contentOrNull ?: return null
val data = json["data"] ?: return null
val provider = EXTENSION_POINT_NAME.extensionList.find { it.serialMarker == marker } ?: return null
return provider.deserialize(data)
}
override fun toString(value: AbstractZigToolchain): String? {
val provider = EXTENSION_POINT_NAME.extensionList.find { it.canSerialize(value) } ?: return null
return buildJsonObject {
put("marker", provider.serialMarker)
put("data", provider.serialize(value))
}.toString()
}
}

View file

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

View file

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

View file

@ -1,104 +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.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.falsepattern.zigbrains.shared.ui.UUIDComboBoxDriver.Companion.wrapModal
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.*
import java.util.function.Supplier
import javax.swing.JComponent
abstract class ZigToolchainConfigurable<T: ZigToolchain>(
val uuid: UUID,
tc: T,
val data: ZigProjectConfigurationProvider.IUserDataBridge?,
private 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 wrapModal(p, modal)
}
override fun getEditableObject(): UUID? {
return uuid
}
override fun getBannerSlogan(): @NlsContexts.DetailedDescription String? {
return displayName
}
override fun getDisplayName(): @NlsContexts.ConfigurableName String? {
return toolchain.name
}
override fun isModified(): Boolean {
return myViews.any { it.isModified(toolchain) }
}
override fun apply() {
toolchain = myViews.fold(toolchain) { tc, view -> view.apply(tc) ?: tc }
}
override fun reset() {
myViews.forEach { it.reset(toolchain) }
}
override fun disposeUIResources() {
myViews.forEach { it.dispose() }
myViews = emptyList()
super.disposeUIResources()
}
companion object {
val TOOLCHAIN_KEY: Key<Supplier<ZigToolchain?>> = Key.create("TOOLCHAIN")
}
}

View file

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

View file

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

View file

@ -1,38 +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.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.local.suggestedLocalToolchainPath
import com.falsepattern.zigbrains.shared.downloader.Downloader
import java.awt.Component
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 val suggestedPath get() = suggestedLocalToolchainPath
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()
}

View file

@ -1,88 +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.downloader
import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.toolchain.local.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.zigToolchainList
import com.falsepattern.zigbrains.shared.downloader.LocalSelector
import com.falsepattern.zigbrains.shared.withUniqueName
import com.intellij.icons.AllIcons
import com.intellij.openapi.fileChooser.FileChooserDescriptor
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.TaskCancellation
import com.intellij.platform.ide.progress.withModalProgress
import java.awt.Component
import java.nio.file.Path
class LocalToolchainSelector(component: Component): LocalSelector<LocalZigToolchain>(component) {
override val windowTitle: String
get() = ZigBrainsBundle.message("settings.toolchain.local-selector.title")
override val descriptor: FileChooserDescriptor
get() = FileChooserDescriptorFactory.createSingleFolderDescriptor()
.withTitle(ZigBrainsBundle.message("settings.toolchain.local-selector.chooser.title"))
override suspend fun verify(path: Path): VerifyResult {
var tc = resolve(path, null)
var result: VerifyResult
if (tc == null) {
result = VerifyResult(
null,
false,
AllIcons.General.Error,
ZigBrainsBundle.message("settings.toolchain.local-selector.state.invalid"),
)
} else {
val existingToolchain = zigToolchainList
.mapNotNull { it.second as? LocalZigToolchain }
.firstOrNull { it.location == tc.location }
if (existingToolchain != null) {
result = VerifyResult(
null,
true,
AllIcons.General.Warning,
existingToolchain.name?.let { ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-named", it) }
?: ZigBrainsBundle.message("settings.toolchain.local-selector.state.already-exists-unnamed")
)
} else {
result = VerifyResult(
null,
true,
AllIcons.General.Information,
ZigBrainsBundle.message("settings.toolchain.local-selector.state.ok")
)
}
}
if (tc != null) {
tc = zigToolchainList.withUniqueName(tc)
}
return result.copy(name = tc?.name)
}
override suspend fun resolve(path: Path, name: String?): LocalZigToolchain? {
return runCatching { withModalProgress(ModalTaskOwner.component(component), "Resolving toolchain", TaskCancellation.cancellable()) {
LocalZigToolchain.tryFromPath(path)?.let { it.withName(name ?: it.name) }
} }.getOrNull()
}
}

View file

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

View file

@ -1,165 +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.local
import com.falsepattern.zigbrains.ZigBrainsBundle
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.util.Disposer
import com.intellij.openapi.util.io.toNioPathOrNull
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.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 LocalZigToolchainPanel() : ImmutableNamedElementPanelBase<LocalZigToolchain>() {
private val pathToToolchain = textFieldWithBrowseButton(
null,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-toolchain"))
).also {
it.textField.document.addDocumentListener(object : DocumentAdapter() {
override fun textChanged(e: DocumentEvent) {
dispatchUpdateUI()
}
})
Disposer.register(this, it)
}
private val toolchainVersion = JBTextArea().also { it.isEditable = false }
private val stdFieldOverride = JBCheckBox().apply {
addChangeListener {
if (isSelected) {
pathToStd.isEnabled = true
} else {
pathToStd.isEnabled = false
updateUI()
}
}
}
private val pathToStd = textFieldWithBrowseButton(
null,
FileChooserDescriptorFactory.createSingleFolderDescriptor().withTitle(ZigBrainsBundle.message("dialog.title.zig-std"))
).also { Disposer.register(this, it) }
private var debounce: Job? = null
override fun attach(panel: Panel): Unit = with(panel) {
super.attach(panel)
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(elem: 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 != elem.name || elem.location != location || elem.std != std
}
override fun apply(elem: 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 elem.copy(location = location, std = std, name = nameFieldValue ?: "")
}
override fun reset(elem: LocalZigToolchain?) {
nameFieldValue = elem?.name ?: ""
this.pathToToolchain.text = elem?.location?.pathString ?: ""
val std = elem?.std
if (std != null) {
stdFieldOverride.isSelected = true
pathToStd.text = std.pathString
pathToStd.isEnabled = true
} else {
stdFieldOverride.isSelected = false
pathToStd.text = ""
pathToStd.isEnabled = false
dispatchUpdateUI()
}
}
private fun dispatchUpdateUI() {
debounce?.cancel("New debounce")
debounce = zigCoroutineScope.launch {
updateUI()
}
}
private suspend fun updateUI() {
delay(200)
val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull()
if (pathToToolchain == null) {
withEDTContext(ModalityState.any()) {
toolchainVersion.text = "[toolchain path empty or invalid]"
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
}
}
return
}
val toolchain = LocalZigToolchain(pathToToolchain)
val zig = toolchain.zig
val env = zig.getEnv(null).getOrElse { throwable ->
throwable.printStackTrace()
withEDTContext(ModalityState.any()) {
toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}"
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
}
}
return
}
val version = env.version
val stdPath = env.stdPath(toolchain, null)
withEDTContext(ModalityState.any()) {
toolchainVersion.text = version
toolchainVersion.foreground = JBColor.foreground()
if (!stdFieldOverride.isSelected) {
pathToStd.text = stdPath?.pathString ?: ""
}
}
}
override fun dispose() {
debounce?.cancel("Disposed")
}
}

View file

@ -1,141 +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.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.downloader.homePath
import com.falsepattern.zigbrains.shared.downloader.xdgDataHome
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.toNioPathOrNull
import com.intellij.ui.SimpleColoredComponent
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
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 = wellKnown.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)
}
}
val suggestedLocalToolchainPath: Path? by lazy {
wellKnown.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 val wellKnown: List<Path> by lazy {
val res = ArrayList<Path>()
xdgDataHome?.let {
res.add(it.resolve("zig"))
res.add(it.resolve("zigup"))
}
homePath?.let { res.add(it.resolve(".zig")) }
res
}

View file

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

View file

@ -0,0 +1,108 @@
/*
* 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.stdlib
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.project.settings.ZigProjectSettings
import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.intellij.navigation.ItemPresentation
import com.intellij.openapi.project.Project
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 kotlinx.coroutines.runBlocking
import java.util.*
import javax.swing.Icon
class ZigSyntheticLibrary(val project: Project) : SyntheticLibrary(), ItemPresentation {
private val roots by lazy {
getRoots(project.zigProjectSettings.state, project)
}
private val name by lazy {
getName(project.zigProjectSettings.state, project)
}
override fun equals(other: Any?): Boolean {
if (other !is ZigSyntheticLibrary)
return false
return roots == other.roots
}
override fun hashCode(): Int {
return Objects.hash(roots)
}
override fun getPresentableText(): String {
return name
}
override fun getIcon(unused: Boolean): Icon {
return Icons.Zig
}
override fun getSourceRoots(): Collection<VirtualFile> {
return roots
}
}
private fun getName(
state: ZigProjectSettings,
project: Project
): String {
val tc = state.toolchain ?: return "Zig"
val version = runBlocking { tc.zig.getEnv(project).version }
return "Zig $version"
}
private fun getRoots(
state: ZigProjectSettings,
project: Project
): Set<VirtualFile> {
val toolchain = state.toolchain
if (state.overrideStdPath) run {
val ePathStr = state.explicitPathToStd ?: return@run
val ePath = ePathStr.toNioPathOrNull() ?: return@run
if (ePath.isAbsolute) {
val roots = ePath.refreshAndFindVirtualDirectory() ?: return@run
return setOf(roots)
} else if (toolchain != null) {
val stdPath = toolchain.location.resolve(ePath)
if (stdPath.isAbsolute) {
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return@run
return setOf(roots)
}
}
}
if (toolchain != null) {
val stdPath = runBlocking { toolchain.zig.getEnv(project) }.stdPath(toolchain, project) ?: return emptySet()
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return emptySet()
return setOf(roots)
}
return emptySet()
}

View file

@ -22,13 +22,13 @@
package com.falsepattern.zigbrains.project.toolchain.tools
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSerializable
import com.intellij.openapi.project.Project
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import java.nio.file.Path
class ZigCompilerTool(toolchain: ZigToolchain) : ZigTool(toolchain) {
class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) {
override val toolName: String
get() = "zig"
@ -36,13 +36,8 @@ class ZigCompilerTool(toolchain: ZigToolchain) : ZigTool(toolchain) {
return toolchain.pathToExecutable(toolName)
}
suspend fun getEnv(project: Project?): Result<ZigToolchainEnvironmentSerializable> {
val stdout = callWithArgs(toolchain.workingDirectory(project), "env").getOrElse { throwable -> return Result.failure(throwable) }.stdout
return try {
Result.success(envJson.decodeFromString<ZigToolchainEnvironmentSerializable>(stdout))
} catch (e: SerializationException) {
Result.failure(IllegalStateException("could not deserialize zig env", e))
}
suspend fun getEnv(project: Project?): ZigToolchainEnvironmentSerializable {
return envJson.decodeFromString<ZigToolchainEnvironmentSerializable>(callWithArgs(toolchain.workingDirectory(project), "env").stdout)
}
}

View file

@ -22,34 +22,49 @@
package com.falsepattern.zigbrains.project.toolchain.tools
import com.falsepattern.zigbrains.project.toolchain.base.ZigToolchain
import com.falsepattern.zigbrains.shared.cli.call
import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.project.Project
import com.intellij.util.io.awaitExit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import java.nio.file.Path
import kotlin.io.path.isRegularFile
abstract class ZigTool(val toolchain: ZigToolchain) {
abstract class ZigTool(val toolchain: AbstractZigToolchain) {
abstract val toolName: String
suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result<ProcessOutput> {
val cli = createBaseCommandLine(workingDirectory, *parameters).let { it.getOrElse { return Result.failure(it) } }
return cli.call(timeoutMillis, ipcProject = ipcProject)
}
suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE): ProcessOutput {
val cli = createBaseCommandLine(workingDirectory, *parameters)
fun fileValid(): Boolean {
val exe = toolchain.pathToExecutable(toolName)
return exe.isRegularFile()
val (process, exitCode) = withContext(Dispatchers.IO) {
val process = cli.createProcess()
val exit = withTimeoutOrNull(timeoutMillis) {
process.awaitExit()
}
process to exit
}
return runInterruptible {
ProcessOutput(
process.inputStream.bufferedReader().use { it.readText() },
process.errorStream.bufferedReader().use { it.readText() },
exitCode ?: -1,
exitCode == null,
false
)
}
}
private suspend fun createBaseCommandLine(
workingDirectory: Path?,
vararg parameters: String
): Result<GeneralCommandLine> {
val exe = toolchain.pathToExecutable(toolName)
return createCommandLineSafe(workingDirectory, exe, *parameters)
.mapCatching { toolchain.patchCommandLine(it) }
): GeneralCommandLine {
val cli = GeneralCommandLine()
.withExePath(toolchain.pathToExecutable(toolName).toString())
.withWorkingDirectory(workingDirectory)
.withParameters(*parameters)
.withCharset(Charsets.UTF_8)
return toolchain.patchCommandLine(cli)
}
}

View file

@ -1,42 +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.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.*
internal object ZigToolchainComboBoxHandler {
@RequiresBackgroundThread
suspend fun onItemSelected(context: Component, elem: ListElem.Pseudo<ZigToolchain>): UUID? = when(elem) {
is ListElem.One.Suggested -> zigToolchainList.withUniqueName(elem.instance)
is ListElem.Download -> LocalToolchainDownloader(context).download()
is ListElem.FromDisk -> LocalToolchainSelector(context).browse()
}?.let { zigToolchainList.registerNew(it) }
}

View file

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

View file

@ -1,141 +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.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.*
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(panel: Panel): Unit = with(panel) {
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(panel) }
}
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 {
if (isEmpty)
return false
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
}
}

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