diff --git a/CHANGELOG.md b/CHANGELOG.md index 787f7aa7..704c5d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ Changelog structure reference: ## [Unreleased] +### Added + +- Project + - Zig std.Progress visualization in the zig tool window (Linux/macOS only) + ## [22.0.1] ### Fixed diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt index 474f49ff..21f93b48 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/base/ZigProfileState.kt @@ -26,7 +26,9 @@ import com.falsepattern.zigbrains.ZigBrainsBundle 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.ipc.IPCUtil import com.falsepattern.zigbrains.shared.coroutine.runModalOrBlocking +import com.falsepattern.zigbrains.shared.ipc.ipc import com.intellij.build.BuildTextConsoleView import com.intellij.execution.DefaultExecutionResult 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.ProcessTerminatedListener import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.openapi.project.Project import com.intellij.platform.ide.progress.ModalTaskOwner import kotlin.io.path.pathString @@ -54,7 +57,7 @@ abstract class ZigProfileState> ( @Throws(ExecutionException::class) suspend fun startProcessSuspend(): ProcessHandler { val toolchain = environment.project.zigProjectSettings.state.toolchain ?: throw ExecutionException(ZigBrainsBundle.message("exception.zig-profile-state.start-process.no-toolchain")) - return ZigProcessHandler(getCommandLine(toolchain, false)) + return startProcess(getCommandLine(toolchain, false), environment.project) } @Throws(ExecutionException::class) @@ -74,16 +77,20 @@ abstract class ZigProfileState> ( } @Throws(ExecutionException::class) -fun executeCommandLine(commandLine: GeneralCommandLine, environment: ExecutionEnvironment): DefaultExecutionResult { - val handler = startProcess(commandLine) +suspend fun executeCommandLine(commandLine: GeneralCommandLine, environment: ExecutionEnvironment): DefaultExecutionResult { + val handler = startProcess(commandLine, environment.project) 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) +suspend fun startProcess(commandLine: GeneralCommandLine, project: Project): ProcessHandler { + val ipc = IPCUtil.wrapWithIPC(commandLine) + val handler = ZigProcessHandler(ipc?.cli ?: commandLine) ProcessTerminatedListener.attach(handler) + if (ipc != null) { + project.ipc?.launchWatcher(ipc, handler.process) + } return handler -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/build/ZigExecConfigBuild.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/build/ZigExecConfigBuild.kt index c77c408b..cf30dfce 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/build/ZigExecConfigBuild.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/execution/build/ZigExecConfigBuild.kt @@ -39,6 +39,10 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx private set var colored = ColoredConfigurable("colored") 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")) private set 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 { val result = ArrayList() result.add("build") - val argsSplit = buildSteps.argsSplit() - val steps = if (debug) { - val truncatedSteps = ArrayList() - 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 + val steps = if (debug) debugBuildSteps.argsSplit() else buildSteps.argsSplit() result.addAll(steps) result.addAll(coloredCliFlags(colored.value, debug)) - result.addAll(extraArgs.argsSplit()) + result.addAll(if (debug) debugExtraArgs.argsSplit() else extraArgs.argsSplit()) return result } @@ -84,7 +75,7 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx override fun getConfigurables(): List> { val baseCfg = super.getConfigurables() + listOf(buildSteps, extraArgs, colored) return if (ZBFeatures.debug()) { - baseCfg + listOf(exePath, exeArgs) + baseCfg + listOf(debugBuildSteps, debugExtraArgs, exePath, exeArgs) } else { baseCfg } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt index 3cf6fc92..86900d6f 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/discovery/ZigStepDiscoveryService.kt @@ -83,7 +83,8 @@ class ZigStepDiscoveryService(private val project: Project) { val result = zig.callWithArgs( project.guessProjectDir()?.toNioPathOrNull(), "build", "-l", - timeoutMillis = currentTimeoutSec * 1000L + timeoutMillis = currentTimeoutSec * 1000L, + ipcProject = project ).getOrElse { throwable -> errorReload(ErrorType.MissingZigExe, throwable.message) null @@ -106,7 +107,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, 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) } else { errorReload(ErrorType.GeneralError, result.stderr) @@ -158,6 +159,6 @@ val Project.zigStepDiscovery get() = service() 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) \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BaseNodeDescriptor.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BaseNodeDescriptor.kt index aa438662..daa35e21 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BaseNodeDescriptor.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BaseNodeDescriptor.kt @@ -28,7 +28,7 @@ import com.intellij.openapi.project.Project import com.intellij.ui.SimpleTextAttributes import javax.swing.Icon -open class BaseNodeDescriptor(project: Project?, displayName: String, displayIcon: Icon, private var description: String? = null): PresentableNodeDescriptor(project, null) { +open class BaseNodeDescriptor(project: Project?, displayName: String, displayIcon: Icon? = null, private var description: String? = null): PresentableNodeDescriptor(project, null) { init { icon = displayIcon myName = displayName diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BuildToolWindowContext.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BuildToolWindowContext.kt index d2d61dc6..6c282fc6 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BuildToolWindowContext.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BuildToolWindowContext.kt @@ -22,7 +22,6 @@ 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 @@ -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.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 @@ -38,47 +41,58 @@ 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 { - val rootNode: DefaultMutableTreeNode = DefaultMutableTreeNode(BaseNodeDescriptor(project, project.name, AllIcons.Actions.ProjectDirectory)) - private val buildZig: DefaultMutableTreeNode = DefaultMutableTreeNode(BaseNodeDescriptor(project, ZigBrainsBundle.message("build.tool.window.tree.steps.label"), Icons.Zig)) - init { - rootNode.add(buildZig) + inner class TreeBox() { + val panel = JPanel(VerticalLayout(0)) + val root = DefaultMutableTreeNode(BaseNodeDescriptor(project, "")) + 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) { - val model = DefaultTreeModel(rootNode) - val tree = Tree(model) - tree.expandPath(TreePath(model.getPathToRoot(buildZig))) - viewport.setViewportView(tree) - tree.addMouseListener(object : MouseAdapter() { + steps.tree.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { 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 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() + val removedNodes = ArrayList() + 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(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 { @@ -120,51 +186,62 @@ class BuildToolWindowContext(private val project: Project): Disposable { c.weighty = 1.0 c.fill = GridBagConstraints.BOTH val viewport = JBScrollPane() - viewport.setViewportNoContent() + viewport.setViewportView(viewPanel) 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 { + withEDTContext(ModalityState.any()) { 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() { - getViewport(project)?.setViewportLoading() + steps.panel.setRunningZigBuild() } - override suspend fun postReload(steps: List>) { - buildZig.removeAllChildren() - for ((task, description) in steps) { + override suspend fun postReload(stepInfo: List>) { + steps.root.removeAllChildren() + for ((task, description) in stepInfo) { val icon = when(task) { "install" -> AllIcons.Actions.Install "uninstall" -> AllIcons.Actions.Uninstall else -> AllIcons.RunConfigurations.TestState.Run } - buildZig.add(DefaultMutableTreeNode(StepNodeDescriptor(project, task, icon, description))) + steps.root.add(DefaultMutableTreeNode(StepNodeDescriptor(project, task, icon, description))) } - withEDTContext { - getViewport(project)?.let { setViewportTree(it) } + withEDTContext(ModalityState.any()) { + steps.model.reload(steps.root) + steps.panel.setViewportBody(steps.tree) } } override suspend fun errorReload(type: ZigStepDiscoveryListener.ErrorType, details: String?) { - withEDTContext { - getViewport(project)?.setViewportError(ZigBrainsBundle.message(when(type) { + withEDTContext(ModalityState.any()) { + steps.panel.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" @@ -174,22 +251,32 @@ class BuildToolWindowContext(private val project: Project): Disposable { } override suspend fun timeoutReload(seconds: Int) { - withEDTContext { - getViewport(project)?.setViewportError(ZigBrainsBundle.message("build.tool.window.status.timeout", seconds), null) + withEDTContext(ModalityState.any()) { + steps.panel.setViewportError(ZigBrainsBundle.message("build.tool.window.status.timeout", seconds), null) } } } } -private fun JBScrollPane.setViewportLoading() { - setViewportView(JBLabel(ZigBrainsBundle.message("build.tool.window.status.loading"), AnimatedIcon.Default(), SwingConstants.CENTER)) +private fun JPanel.setViewportBody(component: Component) { + removeAll() + add(component) + repaint() } -private fun JBScrollPane.setViewportNoContent() { - setViewportView(JBLabel(ZigBrainsBundle.message("build.tool.window.status.not-scanned"), AllIcons.General.Information, SwingConstants.CENTER)) +private fun JPanel.setRunningZigBuild() { + 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() result.layout = BoxLayout(result, BoxLayout.Y_AXIS) 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) result.add(scroll) } - 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) + setViewportBody(result) } private fun getExistingRunConfig(manager: RunManager, stepName: String): RunnerAndConfigurationSettings? { @@ -222,5 +302,3 @@ private fun getExistingRunConfig(manager: RunManager, stepName: String): RunnerA } return null } - -private val VIEWPORT = Key.create("MODEL") diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BuildToolWindowFactory.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BuildToolWindowFactory.kt index 33ba69a8..fd287cf7 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BuildToolWindowFactory.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/steps/ui/BuildToolWindowFactory.kt @@ -23,12 +23,13 @@ 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 { +class BuildToolWindowFactory: ToolWindowFactory, DumbAware { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { project.zigCoroutineScope.launch { BuildToolWindowContext.create(project, toolWindow) diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt index a4dc5170..27b106bf 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/project/toolchain/tools/ZigTool.kt @@ -27,14 +27,15 @@ import com.falsepattern.zigbrains.shared.cli.call import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.ProcessOutput +import com.intellij.openapi.project.Project import java.nio.file.Path abstract class ZigTool(val toolchain: AbstractZigToolchain) { abstract val toolName: String - suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE): Result { + suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result { 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( diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt index 032c7b98..84201c37 100644 --- a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/cli/CLIUtil.kt @@ -23,9 +23,12 @@ package com.falsepattern.zigbrains.shared.cli 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.process.ProcessOutput import com.intellij.openapi.options.ConfigurationException +import com.intellij.openapi.project.Project import com.intellij.util.io.awaitExit import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible @@ -130,9 +133,14 @@ fun createCommandLineSafe( return Result.success(cli) } -suspend fun GeneralCommandLine.call(timeoutMillis: Long = Long.MAX_VALUE): Result { +suspend fun GeneralCommandLine.call(timeoutMillis: Long = Long.MAX_VALUE, ipcProject: Project? = null): Result { + val ipc = if (ipcProject != null) IPCUtil.wrapWithIPC(this) else null + val cli = ipc?.cli ?: this 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) { process.awaitExit() } diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt new file mode 100644 index 00000000..6f5a917d --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/IPCUtil.kt @@ -0,0 +1,82 @@ +/* + * 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 . + */ + +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 argBuilder = StringBuilder() + argBuilder.append("exec {var}>${fifoFile.pathString}; ZIG_PROGRESS=\$var $exePath ${cli.parametersList.parametersString}; exec {var}>&-") + cli.withExePath("bash") + cli.parametersList.clearAll() + cli.addParameters("-c", argBuilder.toString()) + return IPC(cli, fifoFile, fifo) + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/Payload.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/Payload.kt new file mode 100644 index 00000000..4fe34ba7 --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/Payload.kt @@ -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 . + */ + +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 = 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(project, text.toString()) + val self = if (index >= parent.childCount) { + ZigIPCService.IPCTreeNode(descriptor).also { parent.add(it) } + } else { + parent.getChildAt(index).asSafely()?.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) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/ZigIPCService.kt b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/ZigIPCService.kt new file mode 100644 index 00000000..e06440fc --- /dev/null +++ b/core/src/main/kotlin/com/falsepattern/zigbrains/shared/ipc/ZigIPCService.kt @@ -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 . + */ + +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() + val changed = Channel(1) + val mutex = Mutex() + + private fun DataInput.readRoots(): List { + val len = readByte().toUByte().toInt() + val payloads = Array(len) { + readPayload() + } + val parents = ByteArray(len) { + readByte() + } + + val roots = ArrayList() + 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(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() else null \ No newline at end of file diff --git a/core/src/main/resources/zigbrains/Bundle.properties b/core/src/main/resources/zigbrains/Bundle.properties index 49718cae..5adc56ea 100644 --- a/core/src/main/resources/zigbrains/Bundle.properties +++ b/core/src/main/resources/zigbrains/Bundle.properties @@ -108,12 +108,15 @@ settings.project.label.toolchain=Toolchain location settings.project.label.toolchain-version=Detected zig version settings.project.label.override-std=Override standard library settings.project.label.std-location=Standard library location +toolwindow.stripe.zigbrains.build=Zig 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.loading=Running zig build -l 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-zig-exe=Zig executable missing build.tool.window.status.error.general=Error while running zig build -l +build.tool.window.status.no-builds=No builds currently in progress build.tool.window.status.timeout=zig build -l timed out after {0} seconds. -zig=Zig \ No newline at end of file +zig=Zig