From c6af369b1c2bbcb917b9820e341fc4bc7c4d5f7d Mon Sep 17 00:00:00 2001 From: FalsePattern Date: Thu, 13 Mar 2025 16:29:54 +0100 Subject: [PATCH] feat: Version indicator for zls --- CHANGELOG.md | 1 + .../project/toolchain/tools/ZigTool.kt | 40 ++-------- .../zigbrains/shared/cli/CLIUtil.kt | 47 ++++++++++++ .../lsp/settings/ZLSSettingsPanel.kt | 73 ++++++++++++++++++- .../resources/zigbrains/lsp/Bundle.properties | 3 +- 5 files changed, 125 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0769a8..9163b1c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Changelog structure reference: - LSP - Error/Warning banner at the top of the editor when ZLS is misconfigured/not running + - ZLS version indicator in the zig settings - Toolchain - More descriptive error messages when toolchain detection fails 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 3e75d32b..a4dc5170 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 @@ -23,40 +23,18 @@ package com.falsepattern.zigbrains.project.toolchain.tools import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain +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.util.io.awaitExit -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runInterruptible -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeoutOrNull import java.nio.file.Path -import kotlin.io.path.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): Result { val cli = createBaseCommandLine(workingDirectory, *parameters).let { it.getOrElse { return Result.failure(it) } } - - val (process, exitCode) = withContext(Dispatchers.IO) { - val process = cli.createProcess() - val exit = withTimeoutOrNull(timeoutMillis) { - process.awaitExit() - } - process to exit - } - return runInterruptible { - Result.success(ProcessOutput( - process.inputStream.bufferedReader().use { it.readText() }, - process.errorStream.bufferedReader().use { it.readText() }, - exitCode ?: -1, - exitCode == null, - false - )) - } + return cli.call(timeoutMillis) } private suspend fun createBaseCommandLine( @@ -64,15 +42,7 @@ abstract class ZigTool(val toolchain: AbstractZigToolchain) { vararg parameters: String ): Result { val exe = toolchain.pathToExecutable(toolName) - 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 Result.success(toolchain.patchCommandLine(cli)) + return createCommandLineSafe(workingDirectory, exe, *parameters) + .mapCatching { toolchain.patchCommandLine(it) } } } \ No newline at end of file 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 3836fd34..032c7b98 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,8 +23,19 @@ package com.falsepattern.zigbrains.shared.cli import com.falsepattern.zigbrains.ZigBrainsBundle +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.ProcessOutput import com.intellij.openapi.options.ConfigurationException +import com.intellij.util.io.awaitExit +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runInterruptible +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import java.nio.file.Path import java.util.* +import kotlin.io.path.exists +import kotlin.io.path.isDirectory +import kotlin.io.path.pathString //From Apache Ant @@ -100,4 +111,40 @@ fun coloredCliFlags(colored: Boolean, debug: Boolean): List { } else { listOf("--color", if (colored) "on" else "off") } +} + +fun createCommandLineSafe( + workingDirectory: Path?, + exe: Path, + vararg parameters: String, +): Result { + 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 Result.success(cli) +} + +suspend fun GeneralCommandLine.call(timeoutMillis: Long = Long.MAX_VALUE): Result { + val (process, exitCode) = withContext(Dispatchers.IO) { + val process = createProcess() + val exit = withTimeoutOrNull(timeoutMillis) { + process.awaitExit() + } + process to exit + } + return runInterruptible { + Result.success(ProcessOutput( + process.inputStream.bufferedReader().use { it.readText() }, + process.errorStream.bufferedReader().use { it.readText() }, + exitCode ?: -1, + exitCode == null, + false + )) + } } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt index 7843bf6a..c53344d5 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsPanel.kt @@ -29,24 +29,38 @@ import com.falsepattern.zigbrains.direnv.getDirenv import com.falsepattern.zigbrains.lsp.ZLSBundle import com.falsepattern.zigbrains.lsp.config.SemanticTokens import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider +import com.falsepattern.zigbrains.shared.cli.call +import com.falsepattern.zigbrains.shared.cli.createCommandLineSafe import com.falsepattern.zigbrains.shared.coroutine.launchWithEDT +import com.falsepattern.zigbrains.shared.coroutine.withEDTContext import com.falsepattern.zigbrains.shared.zigCoroutineScope +import com.intellij.execution.processTools.mapFlat +import com.intellij.openapi.application.ModalityState import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory import com.intellij.openapi.project.Project +import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.toNioPathOrNull +import com.intellij.openapi.vfs.toNioPathOrNull import com.intellij.platform.ide.progress.ModalTaskOwner import com.intellij.platform.ide.progress.TaskCancellation import com.intellij.platform.ide.progress.withModalProgress +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.JBColor import com.intellij.ui.components.JBCheckBox +import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.fields.ExtendableTextField import com.intellij.ui.components.textFieldWithBrowseButton import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.Panel import com.intellij.ui.dsl.builder.Row +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.jetbrains.annotations.PropertyKey -import java.lang.IllegalArgumentException -import java.util.* +import javax.swing.event.DocumentEvent import kotlin.io.path.pathString @Suppress("PrivatePropertyName") @@ -55,13 +69,24 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr project, FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() .withTitle(ZLSBundle.message("settings.zls-path.browse.title")), - ).also { Disposer.register(this, it) } + ).also { + it.textField.document.addDocumentListener(object: DocumentAdapter() { + override fun textChanged(p0: DocumentEvent) { + dispatchUpdateUI() + } + }) + Disposer.register(this, it) + } private val zlsConfigPath = textFieldWithBrowseButton( project, FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor() .withTitle(ZLSBundle.message("settings.zls-config-path.browse.title")) ).also { Disposer.register(this, it) } + private val zlsVersion = JBTextArea().also { it.isEditable = false } + + private var debounce: Job? = null + private val inlayHints = JBCheckBox() private val enable_snippets = JBCheckBox() private val enable_argument_placeholders = JBCheckBox() @@ -102,6 +127,9 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr cell(direnv) } } + row(ZLSBundle.message("settings.zls-version.label")) { + cell(zlsVersion) + } fancyRow( "settings.zls-config-path.label", "settings.zls-config-path.tooltip" @@ -250,6 +278,7 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr builtin_path.text = value.builtin_path ?: "" build_runner_path.text = value.build_runner_path ?: "" global_cache_path.text = value.global_cache_path ?: "" + dispatchUpdateUI() } private fun dispatchAutodetect(force: Boolean) { @@ -265,12 +294,14 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr getDirenv().findExecutableOnPATH("zls")?.let { if (force || zlsPath.text.isBlank()) { zlsPath.text = it.pathString + dispatchUpdateUI() } } } } override fun dispose() { + debounce?.cancel("Disposed") } private suspend fun getDirenv(): Env { @@ -278,6 +309,42 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr return project.getDirenv() return emptyEnv } + + private fun dispatchUpdateUI() { + debounce?.cancel("New debounce") + debounce = project.zigCoroutineScope.launch { + updateUI() + } + } + + private suspend fun updateUI() { + if (project.isDefault) + return + delay(200) + val zlsPath = this.zlsPath.text.ifBlank { null }?.toNioPathOrNull() + if (zlsPath == null) { + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[zls path empty or invalid]" + } + return + } + val workingDir = project.guessProjectDir()?.toNioPathOrNull() + val result = createCommandLineSafe(workingDir, zlsPath, "version") + .map { it.withEnvironment(getDirenv().env) } + .mapFlat { it.call() } + .getOrElse { throwable -> + throwable.printStackTrace() + withEDTContext(ModalityState.any()) { + zlsVersion.text = "[failed to run \"zls version\"]\n${throwable.message}" + } + return + } + val version = result.stdout + withEDTContext(ModalityState.any()) { + zlsVersion.text = version + zlsVersion.foreground = JBColor.foreground() + } + } } private fun Panel.fancyRow( diff --git a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties index 479fd925..9582f4e9 100644 --- a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties +++ b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties @@ -1,7 +1,8 @@ settings.group.title=ZLS Settings settings.zls-path.label=Executable path -settings.zls-path.tooltip=Path to the ZLS Binary +settings.zls-path.tooltip=Path to the ZLS Binary settings.zls-path.browse.title=Path to the ZLS Binary +settings.zls-version.label=Detected ZLS version settings.zls-config-path.label=Config path settings.zls-config-path.tooltip=Leave empty to use built-in config generated from the settings below settings.zls-config-path.browse.title=Path to the Custom ZLS Config File (Optional)