backport: 23.0.0

This commit is contained in:
FalsePattern 2025-03-14 11:28:10 +01:00
parent 0ec03d6030
commit 8a7a5aa1cb
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
21 changed files with 485 additions and 87 deletions

View file

@ -17,6 +17,18 @@ Changelog structure reference:
## [Unreleased] ## [Unreleased]
## [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] ## [22.0.1]
### Fixed ### Fixed

View file

@ -38,6 +38,7 @@ and might as well utilize the full semver string for extra information.
## Supporters ## Supporters
- ### [Techatrix](https://github.com/Techatrix) - ### [Techatrix](https://github.com/Techatrix)
- ### [nuxusr](https://github.com/nuxusr)
- gree7 - gree7
- xceno - xceno
- AnErrupTion - AnErrupTion

View file

@ -23,6 +23,8 @@
package com.falsepattern.zigbrains.debugger.runner.base package com.falsepattern.zigbrains.debugger.runner.base
import com.falsepattern.zigbrains.project.run.ZigProcessHandler import com.falsepattern.zigbrains.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ipc
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessEvent
@ -30,6 +32,7 @@ import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessListener import com.intellij.execution.process.ProcessListener
import com.intellij.execution.ui.ConsoleView import com.intellij.execution.ui.ConsoleView
import com.intellij.execution.ui.ConsoleViewContentType import com.intellij.execution.ui.ConsoleViewContentType
import com.intellij.openapi.project.Project
import com.intellij.platform.util.progress.withProgressText import com.intellij.platform.util.progress.withProgressText
import com.intellij.util.io.awaitExit import com.intellij.util.io.awaitExit
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -43,9 +46,14 @@ class PreLaunchProcessListener(val console: ConsoleView) : ProcessListener {
private set private set
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
suspend fun executeCommandLineWithHook(commandLine: GeneralCommandLine): Boolean { suspend fun executeCommandLineWithHook(project: Project, commandLine: GeneralCommandLine): Boolean {
return withProgressText(commandLine.commandLineString) { return withProgressText(commandLine.commandLineString) {
val processHandler = ZigProcessHandler(commandLine) val ipc = IPCUtil.wrapWithIPC(commandLine)
val cli = ipc?.cli ?: commandLine
val processHandler = ZigProcessHandler(cli)
if (ipc != null) {
project.ipc?.launchWatcher(ipc, processHandler.process)
}
this@PreLaunchProcessListener.processHandler = processHandler this@PreLaunchProcessListener.processHandler = processHandler
hook(processHandler) hook(processHandler)
processHandler.startNotify() processHandler.startNotify()

View file

@ -54,7 +54,7 @@ abstract class ZigDebugParametersEmitBinaryBase<ProfileState: ZigProfileState<*>
val exe = tmpDir.resolve("executable") val exe = tmpDir.resolve("executable")
commandLine.addParameters("-femit-bin=${exe.absolutePathString()}") commandLine.addParameters("-femit-bin=${exe.absolutePathString()}")
if (listener.executeCommandLineWithHook(commandLine)) if (listener.executeCommandLineWithHook(profileState.environment.project, commandLine))
throw ExecutionException(ZigDebugBundle.message("debug.base.compile.failed.generic")) throw ExecutionException(ZigDebugBundle.message("debug.base.compile.failed.generic"))
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {

View file

@ -82,6 +82,7 @@ abstract class ZigDebugRunnerBase<ProfileState : ZigProfileState<*>> : ZigProgra
} catch (e: ExecutionException) { } catch (e: ExecutionException) {
console.print("\n", ConsoleViewContentType.ERROR_OUTPUT) console.print("\n", ConsoleViewContentType.ERROR_OUTPUT)
e.message?.let { listener.console.print(it, ConsoleViewContentType.SYSTEM_OUTPUT) } e.message?.let { listener.console.print(it, ConsoleViewContentType.SYSTEM_OUTPUT) }
throw e;
} }
if (listener.isBuildFailed) { if (listener.isBuildFailed) {
val executionResult = DefaultExecutionResult(console, listener.processHandler) val executionResult = DefaultExecutionResult(console, listener.processHandler)

View file

@ -61,7 +61,7 @@ class ZigDebugParametersBuild(
withProgressText("Building zig project") { withProgressText("Building zig project") {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val commandLine = profileState.getCommandLine(toolchain, true) val commandLine = profileState.getCommandLine(toolchain, true)
if (listener.executeCommandLineWithHook(commandLine)) if (listener.executeCommandLineWithHook(profileState.environment.project, commandLine))
throw ExecutionException(ZigDebugBundle.message("debug.build.compile.failed.generic")) throw ExecutionException(ZigDebugBundle.message("debug.build.compile.failed.generic"))
val cfg = profileState.configuration val cfg = profileState.configuration
val workingDir = cfg.workingDirectory.path val workingDir = cfg.workingDirectory.path

View file

@ -26,7 +26,9 @@ import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.run.ZigProcessHandler import com.falsepattern.zigbrains.project.run.ZigProcessHandler
import com.falsepattern.zigbrains.project.settings.zigProjectSettings import com.falsepattern.zigbrains.project.settings.zigProjectSettings
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking
import com.falsepattern.zigbrains.shared.ipc.ipc
import com.intellij.build.BuildTextConsoleView import com.intellij.build.BuildTextConsoleView
import com.intellij.execution.DefaultExecutionResult import com.intellij.execution.DefaultExecutionResult
import com.intellij.execution.ExecutionException import com.intellij.execution.ExecutionException
@ -36,6 +38,7 @@ import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.process.ProcessHandler import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.project.Project
import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.ModalTaskOwner
import kotlin.io.path.pathString import kotlin.io.path.pathString
@ -54,7 +57,7 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
suspend fun startProcessSuspend(): ProcessHandler { suspend fun startProcessSuspend(): ProcessHandler {
val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain")) val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain"))
return ZigProcessHandler(getCommandLine(toolchain, false)) return startProcess(getCommandLine(toolchain, false), environment.project)
} }
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
@ -74,16 +77,20 @@ abstract class ZigProfileState<T: ZigExecConfig<T>> (
} }
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
fun executeCommandLine(commandLine: GeneralCommandLine, environment: ExecutionEnvironment): DefaultExecutionResult { suspend fun executeCommandLine(commandLine: GeneralCommandLine, environment: ExecutionEnvironment): DefaultExecutionResult {
val handler = startProcess(commandLine) val handler = startProcess(commandLine, environment.project)
val console = BuildTextConsoleView(environment.project, false, emptyList()) val console = BuildTextConsoleView(environment.project, false, emptyList())
console.attachToProcess(handler) console.attachToProcess(handler)
return DefaultExecutionResult(console, handler) return DefaultExecutionResult(console, handler)
} }
@Throws(ExecutionException::class) @Throws(ExecutionException::class)
fun startProcess(commandLine: GeneralCommandLine): ProcessHandler { suspend fun startProcess(commandLine: GeneralCommandLine, project: Project): ProcessHandler {
val handler = ZigProcessHandler(commandLine) val ipc = IPCUtil.wrapWithIPC(commandLine)
val handler = ZigProcessHandler(ipc?.cli ?: commandLine)
ProcessTerminatedListener.attach(handler) ProcessTerminatedListener.attach(handler)
if (ipc != null) {
project.ipc?.launchWatcher(ipc, handler.process)
}
return handler return handler
} }

View file

@ -39,6 +39,10 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
private set private set
var colored = ColoredConfigurable("colored") var colored = ColoredConfigurable("colored")
private set 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"))
private set
var exePath = FilePathConfigurable("exePath", ZigBrainsBundle.message("exec.option.label.build.exe-path-debug")) var exePath = FilePathConfigurable("exePath", ZigBrainsBundle.message("exec.option.label.build.exe-path-debug"))
private set private set
var exeArgs = ArgsConfigurable("exeArgs", ZigBrainsBundle.message("exec.option.label.build.exe-args-debug")) var exeArgs = ArgsConfigurable("exeArgs", ZigBrainsBundle.message("exec.option.label.build.exe-args-debug"))
@ -48,23 +52,10 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> { override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
val result = ArrayList<String>() val result = ArrayList<String>()
result.add("build") result.add("build")
val argsSplit = buildSteps.argsSplit() val steps = if (debug) debugBuildSteps.argsSplit() else buildSteps.argsSplit()
val steps = if (debug) {
val truncatedSteps = ArrayList<String>()
for (step in argsSplit) {
if (step == "run")
continue
if (step == "test")
throw ExecutionException(ZigBrainsBundle.message("exception.zig-build.debug.test-not-supported"))
truncatedSteps.add(step)
}
truncatedSteps
} else argsSplit
result.addAll(steps) result.addAll(steps)
result.addAll(coloredCliFlags(colored.value, debug)) result.addAll(coloredCliFlags(colored.value, debug))
result.addAll(extraArgs.argsSplit()) result.addAll(if (debug) debugExtraArgs.argsSplit() else extraArgs.argsSplit())
return result return result
} }
@ -84,7 +75,7 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
override fun getConfigurables(): List<ZigConfigurable<*>> { override fun getConfigurables(): List<ZigConfigurable<*>> {
val baseCfg = super.getConfigurables() + listOf(buildSteps, extraArgs, colored) val baseCfg = super.getConfigurables() + listOf(buildSteps, extraArgs, colored)
return if (ZBFeatures.debug()) { return if (ZBFeatures.debug()) {
baseCfg + listOf(exePath, exeArgs) baseCfg + listOf(debugBuildSteps, debugExtraArgs, exePath, exeArgs)
} else { } else {
baseCfg baseCfg
} }

View file

@ -92,7 +92,7 @@ class ZigNewProjectPanel(private var handleGit: Boolean): Disposable {
private val defaultTemplates get() = listOf( private val defaultTemplates get() = listOf(
ZigExecutableTemplate(), // ZigExecutableTemplate(),
ZigLibraryTemplate(), // ZigLibraryTemplate(),
ZigInitTemplate() ZigInitTemplate()
) )

View file

@ -83,7 +83,8 @@ class ZigStepDiscoveryService(private val project: Project) {
val result = zig.callWithArgs( val result = zig.callWithArgs(
project.guessProjectDir()?.toNioPathOrNull(), project.guessProjectDir()?.toNioPathOrNull(),
"build", "-l", "build", "-l",
timeoutMillis = currentTimeoutSec * 1000L timeoutMillis = currentTimeoutSec * 1000L,
ipcProject = project
).getOrElse { throwable -> ).getOrElse { throwable ->
errorReload(ErrorType.MissingZigExe, throwable.message) errorReload(ErrorType.MissingZigExe, throwable.message)
null null
@ -106,7 +107,7 @@ class ZigStepDiscoveryService(private val project: Project) {
} else if (result.isTimeout) { } else if (result.isTimeout) {
timeoutReload(currentTimeoutSec) timeoutReload(currentTimeoutSec)
currentTimeoutSec *= 2 currentTimeoutSec *= 2
} else if (result.stderrLines.any { it.contains("error: no build.zig file found, in the current directory or any parent directories") }) { } else if (result.stderrLines.any { it.contains("error: no build.zig file found") }) {
errorReload(ErrorType.MissingBuildZig, result.stderr) errorReload(ErrorType.MissingBuildZig, result.stderr)
} else { } else {
errorReload(ErrorType.GeneralError, result.stderr) errorReload(ErrorType.GeneralError, result.stderr)
@ -158,6 +159,6 @@ val Project.zigStepDiscovery get() = service<ZigStepDiscoveryService>()
private val SPACES = Regex("\\s+") private val SPACES = Regex("\\s+")
private const val DEFAULT_TIMEOUT_SEC = 10 private const val DEFAULT_TIMEOUT_SEC = 32
private val LOG = Logger.getInstance(ZigStepDiscoveryService::class.java) 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 com.intellij.ui.SimpleTextAttributes
import javax.swing.Icon import javax.swing.Icon
open class BaseNodeDescriptor<T>(project: Project?, displayName: String, displayIcon: Icon, private var description: String? = null): PresentableNodeDescriptor<T>(project, null) { open class BaseNodeDescriptor<T>(project: Project?, displayName: String, displayIcon: Icon? = null, private var description: String? = null): PresentableNodeDescriptor<T>(project, null) {
init { init {
icon = displayIcon icon = displayIcon
myName = displayName myName = displayName

View file

@ -22,7 +22,6 @@
package com.falsepattern.zigbrains.project.steps.ui package com.falsepattern.zigbrains.project.steps.ui
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.project.execution.build.ZigConfigTypeBuild import com.falsepattern.zigbrains.project.execution.build.ZigConfigTypeBuild
import com.falsepattern.zigbrains.project.execution.build.ZigExecConfigBuild import com.falsepattern.zigbrains.project.execution.build.ZigExecConfigBuild
@ -30,6 +29,10 @@ import com.falsepattern.zigbrains.project.execution.firstConfigFactory
import com.falsepattern.zigbrains.project.steps.discovery.ZigStepDiscoveryListener import com.falsepattern.zigbrains.project.steps.discovery.ZigStepDiscoveryListener
import com.falsepattern.zigbrains.project.steps.discovery.zigStepDiscovery import com.falsepattern.zigbrains.project.steps.discovery.zigStepDiscovery
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext 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.ProgramRunnerUtil
import com.intellij.execution.RunManager import com.intellij.execution.RunManager
import com.intellij.execution.RunnerAndConfigurationSettings import com.intellij.execution.RunnerAndConfigurationSettings
@ -38,47 +41,58 @@ import com.intellij.icons.AllIcons
import com.intellij.openapi.Disposable import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.DefaultActionGroup import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.SimpleToolWindowPanel import com.intellij.openapi.ui.SimpleToolWindowPanel
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindow
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.ui.AnimatedIcon import com.intellij.ui.AnimatedIcon
import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBScrollPane import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.ui.content.Content import com.intellij.ui.content.Content
import com.intellij.ui.content.ContentFactory import com.intellij.ui.content.ContentFactory
import com.intellij.ui.treeStructure.Tree 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.GridBagConstraints
import java.awt.GridBagLayout import java.awt.GridBagLayout
import java.awt.event.MouseAdapter import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent import java.awt.event.MouseEvent
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.BoxLayout import javax.swing.BoxLayout
import javax.swing.JPanel import javax.swing.JPanel
import javax.swing.SwingConstants import javax.swing.SwingConstants
import javax.swing.tree.DefaultMutableTreeNode import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeModel import javax.swing.tree.DefaultTreeModel
import javax.swing.tree.MutableTreeNode
import javax.swing.tree.TreePath import javax.swing.tree.TreePath
@OptIn(ExperimentalUnsignedTypes::class)
class BuildToolWindowContext(private val project: Project): Disposable { class BuildToolWindowContext(private val project: Project): Disposable {
val rootNode: DefaultMutableTreeNode = DefaultMutableTreeNode(BaseNodeDescriptor<Any>(project, project.name, AllIcons.Actions.ProjectDirectory)) inner class TreeBox() {
private val buildZig: DefaultMutableTreeNode = DefaultMutableTreeNode(BaseNodeDescriptor<Any>(project, ZigBrainsBundle.message("build.tool.window.tree.steps.label"), Icons.Zig)) val panel = JPanel(VerticalLayout(0))
init { val root = DefaultMutableTreeNode(BaseNodeDescriptor<Any>(project, ""))
rootNode.add(buildZig) val model = DefaultTreeModel(root)
val tree = Tree(model).also { it.isRootVisible = false }
} }
private val viewPanel = JPanel(VerticalLayout(0))
private val steps = TreeBox()
private val build = if (IPCUtil.haveIPC) TreeBox() else null
private var live = AtomicBoolean(true)
init {
viewPanel.add(JBLabel(ZigBrainsBundle.message("build.tool.window.tree.steps.label")))
viewPanel.add(steps.panel)
steps.panel.setNotScanned()
private fun setViewportTree(viewport: JBScrollPane) { steps.tree.addMouseListener(object : MouseAdapter() {
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) { override fun mouseClicked(e: MouseEvent) {
if (e.clickCount == 2) { if (e.clickCount == 2) {
val node = tree.lastSelectedPathComponent as? DefaultMutableTreeNode ?: return val node = steps.tree.lastSelectedPathComponent as? DefaultMutableTreeNode ?: return
val step = node.userObject as? StepNodeDescriptor ?: return val step = node.userObject as? StepNodeDescriptor ?: return
val stepName = step.element?.name ?: return val stepName = step.element?.name ?: return
@ -97,6 +111,58 @@ class BuildToolWindowContext(private val project: Project): Disposable {
} }
} }
}) })
if (build != null) {
viewPanel.add(JBLabel(ZigBrainsBundle.message("build.tool.window.tree.build.label")))
viewPanel.add(build.panel)
build.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()) {
build.root.removeAllChildren()
build.panel.setNoBuilds()
return@withEDTContext
}
val allNodes = ArrayList(ipc.nodes)
val existingNodes = ArrayList<ZigIPCService.IPCTreeNode>()
val removedNodes = ArrayList<ZigIPCService.IPCTreeNode>()
build.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 { build.root.remove(it) }
newNodes.forEach { build.root.add(it) }
if (removedNodes.isNotEmpty() || newNodes.isNotEmpty()) {
build.model.reload(build.root)
}
if (build.root.childCount == 0) {
build.panel.setNoBuilds()
} else {
build.panel.setViewportBody(build.tree)
}
for (bn in allNodes) {
expandRecursively(build, bn)
}
}
}
}
}
}
} }
private fun createContentPanel(): Content { private fun createContentPanel(): Content {
@ -120,51 +186,62 @@ class BuildToolWindowContext(private val project: Project): Disposable {
c.weighty = 1.0 c.weighty = 1.0
c.fill = GridBagConstraints.BOTH c.fill = GridBagConstraints.BOTH
val viewport = JBScrollPane() val viewport = JBScrollPane()
viewport.setViewportNoContent() viewport.setViewportView(viewPanel)
body.add(viewport, c) body.add(viewport, c)
val content = ContentFactory.getInstance().createContent(contentPanel, "", false) val content = ContentFactory.getInstance().createContent(contentPanel, "", false)
content.putUserData(VIEWPORT, viewport)
return content return content
} }
override fun dispose() { override fun dispose() {
live.set(false)
} }
companion object { companion object {
suspend fun create(project: Project, window: ToolWindow) { suspend fun create(project: Project, window: ToolWindow) {
withEDTContext { withEDTContext(ModalityState.any()) {
val context = BuildToolWindowContext(project) val context = BuildToolWindowContext(project)
Disposer.register(context, project.zigStepDiscovery.register(context.BuildReloadListener())) Disposer.register(context, project.zigStepDiscovery.register(context.BuildReloadListener()))
Disposer.register(window.disposable, context) Disposer.register(window.disposable, context)
window.contentManager.addContent(context.createContentPanel()) 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 { inner class BuildReloadListener: ZigStepDiscoveryListener {
override suspend fun preReload() { override suspend fun preReload() {
getViewport(project)?.setViewportLoading() steps.panel.setRunningZigBuild()
} }
override suspend fun postReload(steps: List<Pair<String, String?>>) { override suspend fun postReload(stepInfo: List<Pair<String, String?>>) {
buildZig.removeAllChildren() steps.root.removeAllChildren()
for ((task, description) in steps) { for ((task, description) in stepInfo) {
val icon = when(task) { val icon = when(task) {
"install" -> AllIcons.Actions.Install "install" -> AllIcons.Actions.Install
"uninstall" -> AllIcons.Actions.Uninstall "uninstall" -> AllIcons.Actions.Uninstall
else -> AllIcons.RunConfigurations.TestState.Run else -> AllIcons.RunConfigurations.TestState.Run
} }
buildZig.add(DefaultMutableTreeNode(StepNodeDescriptor(project, task, icon, description))) steps.root.add(DefaultMutableTreeNode(StepNodeDescriptor(project, task, icon, description)))
} }
withEDTContext { withEDTContext(ModalityState.any()) {
getViewport(project)?.let { setViewportTree(it) } steps.model.reload(steps.root)
steps.panel.setViewportBody(steps.tree)
} }
} }
override suspend fun errorReload(type: ZigStepDiscoveryListener.ErrorType, details: String?) { override suspend fun errorReload(type: ZigStepDiscoveryListener.ErrorType, details: String?) {
withEDTContext { withEDTContext(ModalityState.any()) {
getViewport(project)?.setViewportError(ZigBrainsBundle.message(when(type) { steps.panel.setViewportError(ZigBrainsBundle.message(when(type) {
ZigStepDiscoveryListener.ErrorType.MissingToolchain -> "build.tool.window.status.error.missing-toolchain" ZigStepDiscoveryListener.ErrorType.MissingToolchain -> "build.tool.window.status.error.missing-toolchain"
ZigStepDiscoveryListener.ErrorType.MissingZigExe -> "build.tool.window.status.error.missing-zig-exe" ZigStepDiscoveryListener.ErrorType.MissingZigExe -> "build.tool.window.status.error.missing-zig-exe"
ZigStepDiscoveryListener.ErrorType.MissingBuildZig -> "build.tool.window.status.error.missing-build-zig" ZigStepDiscoveryListener.ErrorType.MissingBuildZig -> "build.tool.window.status.error.missing-build-zig"
@ -174,22 +251,32 @@ class BuildToolWindowContext(private val project: Project): Disposable {
} }
override suspend fun timeoutReload(seconds: Int) { override suspend fun timeoutReload(seconds: Int) {
withEDTContext { withEDTContext(ModalityState.any()) {
getViewport(project)?.setViewportError(ZigBrainsBundle.message("build.tool.window.status.timeout", seconds), null) steps.panel.setViewportError(ZigBrainsBundle.message("build.tool.window.status.timeout", seconds), null)
} }
} }
} }
} }
private fun JBScrollPane.setViewportLoading() { private fun JPanel.setViewportBody(component: Component) {
setViewportView(JBLabel(ZigBrainsBundle.message("build.tool.window.status.loading"), AnimatedIcon.Default(), SwingConstants.CENTER)) removeAll()
add(component)
repaint()
} }
private fun JBScrollPane.setViewportNoContent() { private fun JPanel.setRunningZigBuild() {
setViewportView(JBLabel(ZigBrainsBundle.message("build.tool.window.status.not-scanned"), AllIcons.General.Information, SwingConstants.CENTER)) setViewportBody(JBLabel(ZigBrainsBundle.message("build.tool.window.status.loading"), AnimatedIcon.Default(), SwingConstants.CENTER))
} }
private fun JBScrollPane.setViewportError(msg: String, details: String?) { 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?) {
val result = JPanel() val result = JPanel()
result.layout = BoxLayout(result, BoxLayout.Y_AXIS) result.layout = BoxLayout(result, BoxLayout.Y_AXIS)
result.add(JBLabel(msg, AllIcons.General.Error, SwingConstants.CENTER)) result.add(JBLabel(msg, AllIcons.General.Error, SwingConstants.CENTER))
@ -200,14 +287,7 @@ private fun JBScrollPane.setViewportError(msg: String, details: String?) {
val scroll = JBScrollPane(code) val scroll = JBScrollPane(code)
result.add(scroll) result.add(scroll)
} }
setViewportView(result) setViewportBody(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? { private fun getExistingRunConfig(manager: RunManager, stepName: String): RunnerAndConfigurationSettings? {
@ -222,5 +302,3 @@ private fun getExistingRunConfig(manager: RunManager, stepName: String): RunnerA
} }
return null return null
} }
private val VIEWPORT = Key.create<JBScrollPane>("MODEL")

View file

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

View file

@ -27,14 +27,15 @@ import com.falsepattern.zigbrains.shared.cli.call
import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.project.Project
import java.nio.file.Path import java.nio.file.Path
abstract class ZigTool(val toolchain: AbstractZigToolchain) { abstract class ZigTool(val toolchain: AbstractZigToolchain) {
abstract val toolName: String abstract val toolName: String
suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE): Result<ProcessOutput> { 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) } } val cli = createBaseCommandLine(workingDirectory, *parameters).let { it.getOrElse { return Result.failure(it) } }
return cli.call(timeoutMillis) return cli.call(timeoutMillis, ipcProject = ipcProject)
} }
private suspend fun createBaseCommandLine( private suspend fun createBaseCommandLine(

View file

@ -23,9 +23,12 @@
package com.falsepattern.zigbrains.shared.cli package com.falsepattern.zigbrains.shared.cli
import com.falsepattern.zigbrains.ZigBrainsBundle import com.falsepattern.zigbrains.ZigBrainsBundle
import com.falsepattern.zigbrains.shared.ipc.IPCUtil
import com.falsepattern.zigbrains.shared.ipc.ipc
import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessOutput import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.options.ConfigurationException import com.intellij.openapi.options.ConfigurationException
import com.intellij.openapi.project.Project
import com.intellij.util.io.awaitExit import com.intellij.util.io.awaitExit
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
@ -130,9 +133,14 @@ fun createCommandLineSafe(
return Result.success(cli) return Result.success(cli)
} }
suspend fun GeneralCommandLine.call(timeoutMillis: Long = Long.MAX_VALUE): Result<ProcessOutput> { suspend fun GeneralCommandLine.call(timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result<ProcessOutput> {
val ipc = if (ipcProject != null) IPCUtil.wrapWithIPC(this) else null
val cli = ipc?.cli ?: this
val (process, exitCode) = withContext(Dispatchers.IO) { val (process, exitCode) = withContext(Dispatchers.IO) {
val process = createProcess() val process = cli.createProcess()
if (ipc != null) {
ipcProject!!.ipc?.launchWatcher(ipc, process)
}
val exit = withTimeoutOrNull(timeoutMillis) { val exit = withTimeoutOrNull(timeoutMillis) {
process.awaitExit() process.awaitExit()
} }

View file

@ -0,0 +1,81 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.shared.ipc
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.io.FileUtil
import com.intellij.util.io.awaitExit
import java.io.File
import java.nio.file.Path
import javax.swing.tree.DefaultMutableTreeNode
import kotlin.io.path.deleteIfExists
import kotlin.io.path.pathString
/**
* Zig build progress node IPC glue code
*/
object IPCUtil {
val haveIPC = checkHaveIPC()
private fun checkHaveIPC(): Boolean {
if (SystemInfo.isWindows) {
return false;
}
val mkfifo = emptyEnv.findExecutableOnPATH("mkfifo")
val bash = emptyEnv.findExecutableOnPATH("bash")
return mkfifo != null && bash != null
}
private suspend fun mkfifo(path: Path): AutoCloseable? {
val cli = GeneralCommandLine("mkfifo", path.pathString)
val process = cli.createProcess()
val exitCode = process.awaitExit()
return if (exitCode == 0) AutoCloseable {
path.deleteIfExists()
} else null
}
data class IPC(val cli: GeneralCommandLine, val fifoPath: Path, val fifoClose: AutoCloseable)
suspend fun wrapWithIPC(cli: GeneralCommandLine): IPC? {
if (!haveIPC)
return null
val fifoFile = FileUtil.createTempFile("zigbrains-ipc-pipe", null, true).toPath()
fifoFile.deleteIfExists()
val fifo = mkfifo(fifoFile)
if (fifo == null) {
fifoFile.deleteIfExists()
return null
}
//FIFO created, hack cli
val exePath = cli.exePath
val args = "exec {var}>${fifoFile.pathString}; ZIG_PROGRESS=\$var $exePath ${cli.parametersList.parametersString}; exec {var}>&-"
cli.withExePath("bash")
cli.parametersList.clearAll()
cli.addParameters("-c", args)
return IPC(cli, fifoFile, fifo)
}
}

View file

@ -0,0 +1,68 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.shared.ipc
import com.falsepattern.zigbrains.project.steps.ui.BaseNodeDescriptor
import com.intellij.openapi.project.Project
import com.intellij.util.asSafely
import java.io.DataInput
import javax.swing.tree.DefaultMutableTreeNode
data class Payload(val completed: UInt, val estimatedTotal: UInt, val name: String, var children: ArrayList<Payload> = ArrayList()) {
companion object {
fun DataInput.readPayload(): Payload {
val completed = readInt().toUInt()
val estimatedTotal = readInt().toUInt()
val name = ByteArray(40) {
readByte()
}
val length = name.indexOf(0).let { if (it == -1) 40 else it }
val nameText = String(name, 0, length)
return Payload(completed, estimatedTotal, nameText)
}
}
fun addWithChildren(project: Project, parent: ZigIPCService.IPCTreeNode, index: Int) {
val text = StringBuilder()
if (estimatedTotal != 0u) {
text.append('[').append(completed).append('/').append(estimatedTotal).append("] ")
} else if (completed != 0u) {
text.append('[').append(completed).append("] ")
}
text.append(name)
val descriptor = BaseNodeDescriptor<Any>(project, text.toString())
val self = if (index >= parent.childCount) {
ZigIPCService.IPCTreeNode(descriptor).also { parent.add(it) }
} else {
parent.getChildAt(index).asSafely<ZigIPCService.IPCTreeNode>()?.also {
(it.userObject as BaseNodeDescriptor<*>).applyFrom(descriptor)
} ?: ZigIPCService.IPCTreeNode(descriptor).also { parent.add(it) }
}
for ((i, child) in children.withIndex()) {
child.addWithChildren(project, self, i)
}
while (self.childCount > children.size) {
self.remove(self.childCount - 1)
}
}
}

View file

@ -0,0 +1,135 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.shared.ipc
import com.falsepattern.zigbrains.Icons
import com.falsepattern.zigbrains.project.steps.ui.BaseNodeDescriptor
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.ipc.Payload.Companion.readPayload
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.google.common.io.LittleEndianDataInputStream
import com.intellij.icons.AllIcons
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import java.io.BufferedInputStream
import java.io.DataInput
import java.io.EOFException
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.MutableTreeNode
import kotlin.io.path.deleteIfExists
import kotlin.io.path.inputStream
@Service(Service.Level.PROJECT)
class ZigIPCService(val project: Project) {
class IPCTreeNode(userObject: Any?): DefaultMutableTreeNode(userObject) {
var changed: Boolean = true
override fun add(newChild: MutableTreeNode?) {
super.add(newChild)
changed = true
}
override fun remove(aChild: MutableTreeNode?) {
super.remove(aChild)
changed = true
}
override fun remove(childIndex: Int) {
super.remove(childIndex)
changed = true
}
}
val nodes = ArrayList<IPCTreeNode>()
val changed = Channel<Unit>(1)
val mutex = Mutex()
private fun DataInput.readRoots(): List<Payload> {
val len = readByte().toUByte().toInt()
val payloads = Array<Payload>(len) {
readPayload()
}
val parents = ByteArray(len) {
readByte()
}
val roots = ArrayList<Payload>()
for (i in 0..len - 1) {
val parent = parents[i].toUByte()
val payload = payloads[i]
if (parent.toUInt() == 255u) {
roots.add(payloads[i])
} else {
payloads[parent.toInt()].children.add(payload)
}
}
return roots
}
private suspend fun watch(ipc: IPCUtil.IPC, process: Process) {
val currentNode = IPCTreeNode(BaseNodeDescriptor<Any>(project, "pid: ${process.pid()}", AllIcons.Actions.InlayGear))
mutex.withLock {
nodes.add(currentNode)
}
withContext(Dispatchers.IO) {
try {
LittleEndianDataInputStream(BufferedInputStream(ipc.fifoPath.inputStream())).use { fifo ->
while (!project.isDisposed && process.isAlive) {
val roots = fifo.readRoots()
mutex.withLock {
for ((id, root) in roots.withIndex()) {
root.addWithChildren(project, currentNode, id)
}
while (currentNode.childCount > roots.size) {
currentNode.remove(currentNode.childCount - 1)
}
}
changed.trySend(Unit)
}
}
} catch (_: EOFException) {
} finally {
mutex.withLock {
nodes.remove(currentNode)
}
changed.trySend(Unit)
ipc.fifoPath.deleteIfExists()
}
}
}
fun launchWatcher(ipc: IPCUtil.IPC, process: Process) {
project.zigCoroutineScope.launch {
watch(ipc, process)
}
}
}
val Project.ipc get() = if (IPCUtil.haveIPC) service<ZigIPCService>() else null

View file

@ -85,8 +85,10 @@ exec.option.label.compiler-args=Extra compiler command line arguments
exec.option.label.exe-args=Output program command line arguments exec.option.label.exe-args=Output program command line arguments
exec.option.label.build.steps=Build steps exec.option.label.build.steps=Build steps
exec.option.label.build.args=Extra command line arguments exec.option.label.build.args=Extra command line arguments
exec.option.label.build.exe-args-debug=Output program command line arguments (debug only) exec.option.label.build.steps-debug=Debug Build steps
exec.option.label.build.exe-path-debug=Output executable created by the build (debug only, autodetect if empty) exec.option.label.build.args-debug=Debug Extra command line arguments
exec.option.label.build.exe-args-debug=Debug output program command line arguments
exec.option.label.build.exe-path-debug=Debug output executable created by the build
exception.zig.empty-file-path=Empty file path exception.zig.empty-file-path=Empty file path
exception.translate-command-line.unbalanced-quotes=Unbalanced quotes in {0} exception.translate-command-line.unbalanced-quotes=Unbalanced quotes in {0}
exception.zig-profile-state.start-process.no-toolchain=Failed to get zig toolchain from project exception.zig-profile-state.start-process.no-toolchain=Failed to get zig toolchain from project
@ -108,12 +110,15 @@ settings.project.label.toolchain=Toolchain location
settings.project.label.toolchain-version=Detected zig version settings.project.label.toolchain-version=Detected zig version
settings.project.label.override-std=Override standard library settings.project.label.override-std=Override standard library
settings.project.label.std-location=Standard library location settings.project.label.std-location=Standard library location
toolwindow.stripe.zigbrains.build=Zig
build.tool.window.tree.steps.label=Steps build.tool.window.tree.steps.label=Steps
build.tool.window.tree.build.label=Active builds
build.tool.window.status.not-scanned=Build steps not yet scanned. Click the refresh button. build.tool.window.status.not-scanned=Build steps not yet scanned. Click the refresh button.
build.tool.window.status.loading=Running zig build -l build.tool.window.status.loading=Running zig build -l
build.tool.window.status.error.missing-build-zig=No build.zig file found build.tool.window.status.error.missing-build-zig=No build.zig file found
build.tool.window.status.error.missing-toolchain=No zig toolchain configured build.tool.window.status.error.missing-toolchain=No zig toolchain configured
build.tool.window.status.error.missing-zig-exe=Zig executable missing build.tool.window.status.error.missing-zig-exe=Zig executable missing
build.tool.window.status.error.general=Error while running zig build -l build.tool.window.status.error.general=Error while running zig build -l
build.tool.window.status.no-builds=No builds currently in progress
build.tool.window.status.timeout=zig build -l timed out after {0} seconds. build.tool.window.status.timeout=zig build -l timed out after {0} seconds.
zig=Zig zig=Zig

View file

@ -1,7 +1,7 @@
pluginName=ZigBrains pluginName=ZigBrains
pluginRepositoryUrl=https://github.com/FalsePattern/ZigBrains pluginRepositoryUrl=https://github.com/FalsePattern/ZigBrains
pluginVersion=22.0.1 pluginVersion=23.0.0
pluginSinceBuild=241 pluginSinceBuild=241
pluginUntilBuild=241.* pluginUntilBuild=241.*
@ -14,7 +14,7 @@ javaVersion=17
runIdeTarget=clion runIdeTarget=clion
lsp4jVersion=0.21.1 lsp4jVersion=0.21.1
lsp4ijVersion=0.9.0 lsp4ijVersion=0.11.0
lsp4ijNightly=false lsp4ijNightly=false
kotlin.stdlib.default.dependency=false kotlin.stdlib.default.dependency=false

View file

@ -339,7 +339,7 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
} }
return return
} }
val version = result.stdout val version = result.stdout.trim()
withEDTContext(ModalityState.any()) { withEDTContext(ModalityState.any()) {
zlsVersion.text = version zlsVersion.text = version
zlsVersion.foreground = JBColor.foreground() zlsVersion.foreground = JBColor.foreground()