feat: Version indicator for zls
This commit is contained in:
parent
2ab3570d08
commit
c6af369b1c
5 changed files with 125 additions and 39 deletions
|
@ -21,6 +21,7 @@ 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
|
||||||
|
- ZLS version indicator in the zig settings
|
||||||
|
|
||||||
- Toolchain
|
- Toolchain
|
||||||
- More descriptive error messages when toolchain detection fails
|
- More descriptive error messages when toolchain detection fails
|
||||||
|
|
|
@ -23,40 +23,18 @@
|
||||||
package com.falsepattern.zigbrains.project.toolchain.tools
|
package com.falsepattern.zigbrains.project.toolchain.tools
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.project.toolchain.AbstractZigToolchain
|
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.configurations.GeneralCommandLine
|
||||||
import com.intellij.execution.process.ProcessOutput
|
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 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 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): 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)
|
||||||
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
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createBaseCommandLine(
|
private suspend fun createBaseCommandLine(
|
||||||
|
@ -64,15 +42,7 @@ abstract class ZigTool(val toolchain: AbstractZigToolchain) {
|
||||||
vararg parameters: String
|
vararg parameters: String
|
||||||
): Result<GeneralCommandLine> {
|
): Result<GeneralCommandLine> {
|
||||||
val exe = toolchain.pathToExecutable(toolName)
|
val exe = toolchain.pathToExecutable(toolName)
|
||||||
if (!exe.exists())
|
return createCommandLineSafe(workingDirectory, exe, *parameters)
|
||||||
return Result.failure(IllegalArgumentException("file does not exist: ${exe.pathString}"))
|
.mapCatching { toolchain.patchCommandLine(it) }
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -23,8 +23,19 @@
|
||||||
package com.falsepattern.zigbrains.shared.cli
|
package com.falsepattern.zigbrains.shared.cli
|
||||||
|
|
||||||
import com.falsepattern.zigbrains.ZigBrainsBundle
|
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.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 java.util.*
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.isDirectory
|
||||||
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
|
|
||||||
//From Apache Ant
|
//From Apache Ant
|
||||||
|
@ -100,4 +111,40 @@ fun coloredCliFlags(colored: Boolean, debug: Boolean): List<String> {
|
||||||
} else {
|
} else {
|
||||||
listOf("--color", if (colored) "on" else "off")
|
listOf("--color", if (colored) "on" else "off")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createCommandLineSafe(
|
||||||
|
workingDirectory: Path?,
|
||||||
|
exe: Path,
|
||||||
|
vararg parameters: String,
|
||||||
|
): Result<GeneralCommandLine> {
|
||||||
|
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<ProcessOutput> {
|
||||||
|
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
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -29,24 +29,38 @@ import com.falsepattern.zigbrains.direnv.getDirenv
|
||||||
import com.falsepattern.zigbrains.lsp.ZLSBundle
|
import com.falsepattern.zigbrains.lsp.ZLSBundle
|
||||||
import com.falsepattern.zigbrains.lsp.config.SemanticTokens
|
import com.falsepattern.zigbrains.lsp.config.SemanticTokens
|
||||||
import com.falsepattern.zigbrains.project.settings.ZigProjectConfigurationProvider
|
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.launchWithEDT
|
||||||
|
import com.falsepattern.zigbrains.shared.coroutine.withEDTContext
|
||||||
import com.falsepattern.zigbrains.shared.zigCoroutineScope
|
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.fileChooser.FileChooserDescriptorFactory
|
||||||
import com.intellij.openapi.project.Project
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.project.guessProjectDir
|
||||||
import com.intellij.openapi.ui.ComboBox
|
import com.intellij.openapi.ui.ComboBox
|
||||||
import com.intellij.openapi.util.Disposer
|
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.ModalTaskOwner
|
||||||
import com.intellij.platform.ide.progress.TaskCancellation
|
import com.intellij.platform.ide.progress.TaskCancellation
|
||||||
import com.intellij.platform.ide.progress.withModalProgress
|
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.JBCheckBox
|
||||||
|
import com.intellij.ui.components.JBTextArea
|
||||||
import com.intellij.ui.components.fields.ExtendableTextField
|
import com.intellij.ui.components.fields.ExtendableTextField
|
||||||
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
|
||||||
import com.intellij.ui.dsl.builder.Row
|
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 org.jetbrains.annotations.PropertyKey
|
||||||
import java.lang.IllegalArgumentException
|
import javax.swing.event.DocumentEvent
|
||||||
import java.util.*
|
|
||||||
import kotlin.io.path.pathString
|
import kotlin.io.path.pathString
|
||||||
|
|
||||||
@Suppress("PrivatePropertyName")
|
@Suppress("PrivatePropertyName")
|
||||||
|
@ -55,13 +69,24 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
|
||||||
project,
|
project,
|
||||||
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
|
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
|
||||||
.withTitle(ZLSBundle.message("settings.zls-path.browse.title")),
|
.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(
|
private val zlsConfigPath = textFieldWithBrowseButton(
|
||||||
project,
|
project,
|
||||||
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
|
FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor()
|
||||||
.withTitle(ZLSBundle.message("settings.zls-config-path.browse.title"))
|
.withTitle(ZLSBundle.message("settings.zls-config-path.browse.title"))
|
||||||
).also { Disposer.register(this, it) }
|
).also { Disposer.register(this, it) }
|
||||||
|
|
||||||
|
private val zlsVersion = JBTextArea().also { it.isEditable = false }
|
||||||
|
|
||||||
|
private var debounce: Job? = null
|
||||||
|
|
||||||
private val inlayHints = JBCheckBox()
|
private val inlayHints = JBCheckBox()
|
||||||
private val enable_snippets = JBCheckBox()
|
private val enable_snippets = JBCheckBox()
|
||||||
private val enable_argument_placeholders = JBCheckBox()
|
private val enable_argument_placeholders = JBCheckBox()
|
||||||
|
@ -102,6 +127,9 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
|
||||||
cell(direnv)
|
cell(direnv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
row(ZLSBundle.message("settings.zls-version.label")) {
|
||||||
|
cell(zlsVersion)
|
||||||
|
}
|
||||||
fancyRow(
|
fancyRow(
|
||||||
"settings.zls-config-path.label",
|
"settings.zls-config-path.label",
|
||||||
"settings.zls-config-path.tooltip"
|
"settings.zls-config-path.tooltip"
|
||||||
|
@ -250,6 +278,7 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
|
||||||
builtin_path.text = value.builtin_path ?: ""
|
builtin_path.text = value.builtin_path ?: ""
|
||||||
build_runner_path.text = value.build_runner_path ?: ""
|
build_runner_path.text = value.build_runner_path ?: ""
|
||||||
global_cache_path.text = value.global_cache_path ?: ""
|
global_cache_path.text = value.global_cache_path ?: ""
|
||||||
|
dispatchUpdateUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun dispatchAutodetect(force: Boolean) {
|
private fun dispatchAutodetect(force: Boolean) {
|
||||||
|
@ -265,12 +294,14 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
|
||||||
getDirenv().findExecutableOnPATH("zls")?.let {
|
getDirenv().findExecutableOnPATH("zls")?.let {
|
||||||
if (force || zlsPath.text.isBlank()) {
|
if (force || zlsPath.text.isBlank()) {
|
||||||
zlsPath.text = it.pathString
|
zlsPath.text = it.pathString
|
||||||
|
dispatchUpdateUI()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
|
debounce?.cancel("Disposed")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDirenv(): Env {
|
private suspend fun getDirenv(): Env {
|
||||||
|
@ -278,6 +309,42 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
|
||||||
return project.getDirenv()
|
return project.getDirenv()
|
||||||
return emptyEnv
|
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(
|
private fun Panel.fancyRow(
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
settings.group.title=ZLS Settings
|
settings.group.title=ZLS Settings
|
||||||
settings.zls-path.label=Executable path
|
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-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.label=Config path
|
||||||
settings.zls-config-path.tooltip=Leave empty to use built-in config generated from the settings below
|
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)
|
settings.zls-config-path.browse.title=Path to the Custom ZLS Config File (Optional)
|
||||||
|
|
Loading…
Add table
Reference in a new issue