feat: Descriptive zig environment errors

This commit is contained in:
FalsePattern 2025-03-13 14:42:41 +01:00
parent 91ee38e922
commit d4a1b69172
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
9 changed files with 60 additions and 47 deletions

View file

@ -22,6 +22,9 @@ Changelog structure reference:
- LSP - LSP
- Error/Warning banner at the top of the editor when ZLS is misconfigured/not running - Error/Warning banner at the top of the editor when ZLS is misconfigured/not running
- Toolchain
- More descriptive error messages when toolchain detection fails
### Fixed ### Fixed
- Debugging - Debugging

View file

@ -73,11 +73,10 @@ data class ZigProjectConfigurationData(
).notify(project) ).notify(project)
return@indeterminateStep false return@indeterminateStep false
} }
val result = zig.callWithArgs(workDir, "init") val result = zig.callWithArgs(workDir, "init").getOrElse { throwable ->
if (result == null) {
Notification( Notification(
"zigbrains", "zigbrains",
"\"zig init\" could not run because the zig executable was missing!", "Failed to run \"zig init\": ${throwable.message}",
NotificationType.ERROR NotificationType.ERROR
).notify(project) ).notify(project)
return@indeterminateStep false return@indeterminateStep false

View file

@ -27,11 +27,11 @@ import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain import com.falsepattern.zigbrains.project.toolchain.LocalZigToolchain
import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider import com.falsepattern.zigbrains.project.toolchain.ZigToolchainProvider
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.Disposable import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.UserDataHolderBase import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.toNioPathOrNull import com.intellij.openapi.util.io.toNioPathOrNull
@ -41,7 +41,7 @@ import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.ui.DocumentAdapter import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBColor import com.intellij.ui.JBColor
import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBLabel import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.components.textFieldWithBrowseButton
import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Panel
@ -67,7 +67,7 @@ class ZigProjectSettingsPanel(private val project: Project) : ZigProjectConfigur
}) })
Disposer.register(this, it) Disposer.register(this, it)
} }
private val toolchainVersion = JBLabel() private val toolchainVersion = JBTextArea().also { it.isEditable = false }
private val stdFieldOverride = JBCheckBox(ZigBrainsBundle.message("settings.project.label.override-std")).apply { private val stdFieldOverride = JBCheckBox(ZigBrainsBundle.message("settings.project.label.override-std")).apply {
addChangeListener { addChangeListener {
if (isSelected) { if (isSelected) {
@ -159,35 +159,39 @@ class ZigProjectSettingsPanel(private val project: Project) : ZigProjectConfigur
} }
} }
private suspend fun updateUI() { private suspend fun updateUI() {
val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull()
delay(200) delay(200)
val toolchain = pathToToolchain?.let { LocalZigToolchain(it) } val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull()
val zig = toolchain?.zig if (pathToToolchain == null) {
if (zig?.path()?.toFile()?.exists() != true) { withEDTContext(ModalityState.any()) {
toolchainVersion.text = "[zig binary not found]" toolchainVersion.text = "[toolchain path empty or invalid]"
if (!stdFieldOverride.isSelected) {
if (!stdFieldOverride.isSelected) { pathToStd.text = ""
pathToStd.text = "" }
} }
return return
} }
val env = zig.getEnv(project) val toolchain = LocalZigToolchain(pathToToolchain)
if (env == null) { val zig = toolchain.zig
toolchainVersion.text = "[failed to run zig env]" val env = zig.getEnv(project).getOrElse { throwable ->
if (!stdFieldOverride.isSelected) { throwable.printStackTrace()
pathToStd.text = "" withEDTContext(ModalityState.any()) {
toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}"
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
}
} }
return return
} }
val version = env.version val version = env.version
val stdPath = env.stdPath(toolchain, project) val stdPath = env.stdPath(toolchain, project)
toolchainVersion.text = version
toolchainVersion.foreground = JBColor.foreground()
if (!stdFieldOverride.isSelected) { withEDTContext(ModalityState.any()) {
pathToStd.text = stdPath?.pathString ?: "" toolchainVersion.text = version
toolchainVersion.foreground = JBColor.foreground()
if (!stdFieldOverride.isSelected) {
pathToStd.text = stdPath?.pathString ?: ""
}
} }
} }

View file

@ -84,9 +84,12 @@ class ZigStepDiscoveryService(private val project: Project) {
project.guessProjectDir()?.toNioPathOrNull(), project.guessProjectDir()?.toNioPathOrNull(),
"build", "-l", "build", "-l",
timeoutMillis = currentTimeoutSec * 1000L timeoutMillis = currentTimeoutSec * 1000L
) ).getOrElse { throwable ->
errorReload(ErrorType.MissingZigExe, throwable.message)
null
}
if (result == null) { if (result == null) {
errorReload(ErrorType.MissingZigExe) {}
} else if (result.checkSuccess(LOG)) { } else if (result.checkSuccess(LOG)) {
currentTimeoutSec = DEFAULT_TIMEOUT_SEC currentTimeoutSec = DEFAULT_TIMEOUT_SEC
val lines = result.stdoutLines val lines = result.stdoutLines

View file

@ -132,7 +132,7 @@ private fun getName(
project: Project project: Project
): String { ): String {
val tc = state.toolchain ?: return "Zig" val tc = state.toolchain ?: return "Zig"
val version = runBlocking { tc.zig.getEnv(project)?.version } ?: return "Zig" val version = runBlocking { tc.zig.getEnv(project) }.mapCatching { it.version }.getOrElse { return "Zig" }
return "Zig $version" return "Zig $version"
} }
@ -156,7 +156,7 @@ suspend fun getRoot(
} }
} }
if (toolchain != null) { if (toolchain != null) {
val stdPath = toolchain.zig.getEnv(project)?.stdPath(toolchain, project) ?: return null val stdPath = toolchain.zig.getEnv(project).mapCatching { it.stdPath(toolchain, project) }.getOrNull() ?: return null
val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null val roots = stdPath.refreshAndFindVirtualDirectory() ?: return null
return roots return roots
} }

View file

@ -27,6 +27,7 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSeria
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.lang.IllegalStateException
import java.nio.file.Path import java.nio.file.Path
class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) { class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) {
@ -37,12 +38,12 @@ class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) {
return toolchain.pathToExecutable(toolName) return toolchain.pathToExecutable(toolName)
} }
suspend fun getEnv(project: Project?): ZigToolchainEnvironmentSerializable? { suspend fun getEnv(project: Project?): Result<ZigToolchainEnvironmentSerializable> {
val stdout = callWithArgs(toolchain.workingDirectory(project), "env")?.stdout ?: return null val stdout = callWithArgs(toolchain.workingDirectory(project), "env").getOrElse { throwable -> return Result.failure(throwable) }.stdout
return try { return try {
envJson.decodeFromString<ZigToolchainEnvironmentSerializable>(stdout) Result.success(envJson.decodeFromString<ZigToolchainEnvironmentSerializable>(stdout))
} catch (e: SerializationException) { } catch (e: SerializationException) {
null Result.failure(IllegalStateException("could not deserialize zig env", e))
} }
} }
} }

View file

@ -31,13 +31,15 @@ import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.isRegularFile import kotlin.io.path.exists
import kotlin.io.path.isDirectory
import kotlin.io.path.pathString
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): ProcessOutput? { suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE): Result<ProcessOutput> {
val cli = createBaseCommandLine(workingDirectory, *parameters) ?: return null val cli = createBaseCommandLine(workingDirectory, *parameters).let { it.getOrElse { return Result.failure(it) } }
val (process, exitCode) = withContext(Dispatchers.IO) { val (process, exitCode) = withContext(Dispatchers.IO) {
val process = cli.createProcess() val process = cli.createProcess()
@ -47,28 +49,30 @@ abstract class ZigTool(val toolchain: AbstractZigToolchain) {
process to exit process to exit
} }
return runInterruptible { return runInterruptible {
ProcessOutput( Result.success(ProcessOutput(
process.inputStream.bufferedReader().use { it.readText() }, process.inputStream.bufferedReader().use { it.readText() },
process.errorStream.bufferedReader().use { it.readText() }, process.errorStream.bufferedReader().use { it.readText() },
exitCode ?: -1, exitCode ?: -1,
exitCode == null, exitCode == null,
false false
) ))
} }
} }
private suspend fun createBaseCommandLine( private suspend fun createBaseCommandLine(
workingDirectory: Path?, workingDirectory: Path?,
vararg parameters: String vararg parameters: String
): GeneralCommandLine? { ): Result<GeneralCommandLine> {
val exe = toolchain.pathToExecutable(toolName) val exe = toolchain.pathToExecutable(toolName)
if (!exe.isRegularFile()) if (!exe.exists())
return null return Result.failure(IllegalArgumentException("file does not exist: ${exe.pathString}"))
if (exe.isDirectory())
return Result.failure(IllegalArgumentException("file is a directory: ${exe.pathString}"))
val cli = GeneralCommandLine() val cli = GeneralCommandLine()
.withExePath(exe.toString()) .withExePath(exe.toString())
.withWorkingDirectory(workingDirectory) .withWorkingDirectory(workingDirectory)
.withParameters(*parameters) .withParameters(*parameters)
.withCharset(Charsets.UTF_8) .withCharset(Charsets.UTF_8)
return toolchain.patchCommandLine(cli) return Result.success(toolchain.patchCommandLine(cli))
} }
} }

View file

@ -105,7 +105,7 @@ configuration.build.marker-name=Build and Run
settings.project.group.title=Zig Settings settings.project.group.title=Zig Settings
settings.project.label.direnv=Use direnv settings.project.label.direnv=Use direnv
settings.project.label.toolchain=Toolchain location settings.project.label.toolchain=Toolchain location
settings.project.label.toolchain-version=Toolchain version settings.project.label.toolchain-version=Detected toolchain 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
build.tool.window.tree.steps.label=Steps build.tool.window.tree.steps.label=Steps

View file

@ -39,12 +39,11 @@ class ToolchainZLSConfigProvider: SuspendingZLSConfigProvider {
var state = svc.state var state = svc.state
val toolchain = state.toolchain ?: ZigToolchainProvider.suggestToolchain(project, UserDataHolderBase()) ?: return previous val toolchain = state.toolchain ?: ZigToolchainProvider.suggestToolchain(project, UserDataHolderBase()) ?: return previous
val env = toolchain.zig.getEnv(project) val env = toolchain.zig.getEnv(project).getOrElse { throwable ->
throwable.printStackTrace()
if (env == null) {
Notification( Notification(
"zigbrains-lsp", "zigbrains-lsp",
"Failed to evaluate zig env", "Failed to evaluate \"zig env\": ${throwable.message}",
NotificationType.ERROR NotificationType.ERROR
).notify(project) ).notify(project)
return previous return previous