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
|
||||
- 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
|
||||
|
|
|
@ -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<ProcessOutput> {
|
||||
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<GeneralCommandLine> {
|
||||
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) }
|
||||
}
|
||||
}
|
|
@ -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<String> {
|
|||
} else {
|
||||
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.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(
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue