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
- 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
- Debugging

View file

@ -73,11 +73,10 @@ data class ZigProjectConfigurationData(
).notify(project)
return@indeterminateStep false
}
val result = zig.callWithArgs(workDir, "init")
if (result == null) {
val result = zig.callWithArgs(workDir, "init").getOrElse { throwable ->
Notification(
"zigbrains",
"\"zig init\" could not run because the zig executable was missing!",
"Failed to run \"zig init\": ${throwable.message}",
NotificationType.ERROR
).notify(project)
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.ZigToolchainProvider
import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.UserDataHolderBase
import com.intellij.openapi.util.io.toNioPathOrNull
@ -41,7 +41,7 @@ import com.intellij.platform.ide.progress.withModalProgress
import com.intellij.ui.DocumentAdapter
import com.intellij.ui.JBColor
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.textFieldWithBrowseButton
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Panel
@ -67,7 +67,7 @@ class ZigProjectSettingsPanel(private val project: Project) : ZigProjectConfigur
})
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 {
addChangeListener {
if (isSelected) {
@ -159,35 +159,39 @@ class ZigProjectSettingsPanel(private val project: Project) : ZigProjectConfigur
}
}
private suspend fun updateUI() {
val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull()
delay(200)
val toolchain = pathToToolchain?.let { LocalZigToolchain(it) }
val zig = toolchain?.zig
if (zig?.path()?.toFile()?.exists() != true) {
toolchainVersion.text = "[zig binary not found]"
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
val pathToToolchain = this.pathToToolchain.text.ifBlank { null }?.toNioPathOrNull()
if (pathToToolchain == null) {
withEDTContext(ModalityState.any()) {
toolchainVersion.text = "[toolchain path empty or invalid]"
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
}
}
return
}
val env = zig.getEnv(project)
if (env == null) {
toolchainVersion.text = "[failed to run zig env]"
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
val toolchain = LocalZigToolchain(pathToToolchain)
val zig = toolchain.zig
val env = zig.getEnv(project).getOrElse { throwable ->
throwable.printStackTrace()
withEDTContext(ModalityState.any()) {
toolchainVersion.text = "[failed to run \"zig env\"]\n${throwable.message}"
if (!stdFieldOverride.isSelected) {
pathToStd.text = ""
}
}
return
}
val version = env.version
val stdPath = env.stdPath(toolchain, project)
toolchainVersion.text = version
toolchainVersion.foreground = JBColor.foreground()
if (!stdFieldOverride.isSelected) {
pathToStd.text = stdPath?.pathString ?: ""
withEDTContext(ModalityState.any()) {
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(),
"build", "-l",
timeoutMillis = currentTimeoutSec * 1000L
)
).getOrElse { throwable ->
errorReload(ErrorType.MissingZigExe, throwable.message)
null
}
if (result == null) {
errorReload(ErrorType.MissingZigExe)
{}
} else if (result.checkSuccess(LOG)) {
currentTimeoutSec = DEFAULT_TIMEOUT_SEC
val lines = result.stdoutLines

View file

@ -132,7 +132,7 @@ private fun getName(
project: Project
): String {
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"
}
@ -156,7 +156,7 @@ suspend fun getRoot(
}
}
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
return roots
}

View file

@ -27,6 +27,7 @@ import com.falsepattern.zigbrains.project.toolchain.ZigToolchainEnvironmentSeria
import com.intellij.openapi.project.Project
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import java.lang.IllegalStateException
import java.nio.file.Path
class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) {
@ -37,12 +38,12 @@ class ZigCompilerTool(toolchain: AbstractZigToolchain) : ZigTool(toolchain) {
return toolchain.pathToExecutable(toolName)
}
suspend fun getEnv(project: Project?): ZigToolchainEnvironmentSerializable? {
val stdout = callWithArgs(toolchain.workingDirectory(project), "env")?.stdout ?: return null
suspend fun getEnv(project: Project?): Result<ZigToolchainEnvironmentSerializable> {
val stdout = callWithArgs(toolchain.workingDirectory(project), "env").getOrElse { throwable -> return Result.failure(throwable) }.stdout
return try {
envJson.decodeFromString<ZigToolchainEnvironmentSerializable>(stdout)
Result.success(envJson.decodeFromString<ZigToolchainEnvironmentSerializable>(stdout))
} 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.withTimeoutOrNull
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 val toolName: String
suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE): ProcessOutput? {
val cli = createBaseCommandLine(workingDirectory, *parameters) ?: return null
suspend fun callWithArgs(workingDirectory: Path?, vararg parameters: String, timeoutMillis: Long = Long.MAX_VALUE): Result<ProcessOutput> {
val cli = createBaseCommandLine(workingDirectory, *parameters).let { it.getOrElse { return Result.failure(it) } }
val (process, exitCode) = withContext(Dispatchers.IO) {
val process = cli.createProcess()
@ -47,28 +49,30 @@ abstract class ZigTool(val toolchain: AbstractZigToolchain) {
process to exit
}
return runInterruptible {
ProcessOutput(
Result.success(ProcessOutput(
process.inputStream.bufferedReader().use { it.readText() },
process.errorStream.bufferedReader().use { it.readText() },
exitCode ?: -1,
exitCode == null,
false
)
))
}
}
private suspend fun createBaseCommandLine(
workingDirectory: Path?,
vararg parameters: String
): GeneralCommandLine? {
): Result<GeneralCommandLine> {
val exe = toolchain.pathToExecutable(toolName)
if (!exe.isRegularFile())
return null
if (!exe.exists())
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()
.withExePath(exe.toString())
.withWorkingDirectory(workingDirectory)
.withParameters(*parameters)
.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.label.direnv=Use direnv
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.std-location=Standard library location
build.tool.window.tree.steps.label=Steps

View file

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