backport: 22.0.0

This commit is contained in:
FalsePattern 2025-03-11 14:29:48 +01:00
parent ae08287d7e
commit 0162e53b01
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
33 changed files with 378 additions and 270 deletions

View file

@ -17,6 +17,31 @@ Changelog structure reference:
## [Unreleased]
## [22.0.0]
### Added
- 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
### Changed
- Project
- !!BREAKING CHANGE!! Changed file format of zig tasks to store command line arguments as strings instead of string lists.
This (and newer) versions of the plugin will automatically upgrade tasks from 21.1.0 and before.
### Fixed
- Debugging
- Breakpoints could not be placed inside zig code in Android Studio
- Project
- Zig run/debug configuration command line arguments would lose quotes around arguments
## [21.1.0]
### Added

View file

@ -1,53 +0,0 @@
# Code of Merit
1. The project creators, lead developers, core team, constitute
the managing members of the project and have final say in every decision
of the project, technical or otherwise, including overruling previous decisions.
There are no limitations to this decisional power.
2. Contributions are an expected result of your membership on the project.
Don't expect others to do your work or help you with your work forever.
3. All members have the same opportunities to seek any challenge they want
within the project.
4. Authority or position in the project will be proportional
to the accrued contribution. Seniority must be earned.
5. Software is evolutive: the better implementations must supersede lesser
implementations. Technical advantage is the primary evaluation metric.
6. This is a space for technical prowess; topics outside of the project
will not be tolerated.
7. Non technical conflicts will be discussed in a separate space. Disruption
of the project will not be allowed.
8. Individual characteristics, including but not limited to,
body, sex, sexual preference, race, language, religion, nationality,
or political preferences are irrelevant in the scope of the project and
will not be taken into account concerning your value or that of your contribution
to the project.
9. Discuss or debate the idea, not the person.
10. There is no room for ambiguity: Ambiguity will be met with questioning;
further ambiguity will be met with silence. It is the responsibility
of the originator to provide requested context.
11. If something is illegal outside the scope of the project, it is illegal
in the scope of the project. This Code of Merit does not take precedence over
governing law.
12. This Code of Merit governs the technical procedures of the project not the
activities outside of it.
13. Participation on the project equates to agreement of this Code of Merit.
14. No objectives beyond the stated objectives of this project are relevant
to the project. Any intent to deviate the project from its original purpose
of existence will constitute grounds for remedial action which may include
expulsion from the project.
This document is adapted from the Code of Merit, version 1.0.
See: https://codeofmerit.org/.

View file

@ -39,7 +39,7 @@ class ZigExecConfigBinary(project: Project, factory: ConfigurationFactory) : Zig
get() = ZigDebugBundle.message("configuration.binary.suggested-name")
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
return args.args
return args.argsSplit()
}
override fun getConfigurables(): List<ZigConfigurable<*>> {

View file

@ -36,6 +36,6 @@ class ZigDebugParametersBinary @Throws(ExecutionException::class) constructor(dr
ZigDebugParametersBase<ZigProfileStateBinary>(driverConfiguration, toolchain, profileState) {
private val executableFile = profileState.configuration.exePath.path?.toFile() ?: throw ExecutionException(ZigDebugBundle.message("exception.missing-exe-path"))
override fun getInstaller(): Installer {
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.args.args)
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.args.argsSplit())
}
}

View file

@ -53,7 +53,7 @@ class ZigDebugParametersBuild(
private lateinit var executableFile: File
override fun getInstaller(): Installer {
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.args)
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit())
}
@Throws(ExecutionException::class)

View file

@ -32,6 +32,6 @@ import com.jetbrains.cidr.execution.debugger.backend.DebuggerDriverConfiguration
class ZigDebugParametersRun(driverConfiguration: DebuggerDriverConfiguration, toolchain: AbstractZigToolchain, profileState: ZigProfileStateRun) :
ZigDebugParametersEmitBinaryBase<ZigProfileStateRun>(driverConfiguration, toolchain, profileState) {
override fun getInstaller(): Installer {
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.args)
return ZigDebugEmitBinaryInstaller(profileState, toolchain, executableFile, profileState.configuration.exeArgs.argsSplit())
}
}

View file

@ -1,5 +1,5 @@
<idea-plugin package="com.falsepattern.zigbrains.debugger">
<depends>com.intellij.nativeDebug</depends>
<depends>com.intellij.modules.cidr.debugger</depends>
<extensions defaultExtensionNs="com.intellij">
<configurationType

View file

@ -347,14 +347,18 @@ class ArgsConfigurable(
@Transient private val serializedName: String,
@Transient @Nls private val guiName: String
) : ZigConfigurable<ArgsConfigurable>, Cloneable {
var args: List<String> = emptyList()
var args: String = ""
override fun readExternal(element: Element) {
args = element.readStrings(serializedName) ?: return
args = element.readString(serializedName) ?: element.readStrings(serializedName)?.joinToString(separator = " ") { if (it.contains(' ')) "\"$it\"" else it } ?: ""
}
fun argsSplit(): List<String> {
return translateCommandline(args)
}
override fun writeExternal(element: Element) {
element.writeStrings(serializedName, args)
element.writeString(serializedName, args)
}
override fun createEditor(): ZigConfigModule<ArgsConfigurable> {
@ -376,12 +380,12 @@ class ArgsConfigurable(
}
override fun apply(configurable: ArgsConfigurable): Boolean {
configurable.args = translateCommandline(argsField.text)
configurable.args = argsField.text ?: ""
return true
}
override fun reset(configurable: ArgsConfigurable) {
argsField.text = configurable.args.joinToString(separator = " ")
argsField.text = configurable.args
}
override fun construct(p: Panel): Unit = with(p) {

View file

@ -48,9 +48,10 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
override suspend fun buildCommandLineArgs(debug: Boolean): List<String> {
val result = ArrayList<String>()
result.add("build")
val argsSplit = buildSteps.argsSplit()
val steps = if (debug) {
val truncatedSteps = ArrayList<String>()
for (step in buildSteps.args) {
for (step in argsSplit) {
if (step == "run")
continue
@ -60,10 +61,10 @@ class ZigExecConfigBuild(project: Project, factory: ConfigurationFactory): ZigEx
truncatedSteps.add(step)
}
truncatedSteps
} else buildSteps.args
} else argsSplit
result.addAll(steps)
result.addAll(coloredCliFlags(colored.value, debug))
result.addAll(extraArgs.args)
result.addAll(extraArgs.argsSplit())
return result
}

View file

@ -52,10 +52,10 @@ class ZigExecConfigRun(project: Project, factory: ConfigurationFactory): ZigExec
if (!debug || optimization.forced) {
result.addAll(listOf("-O", optimization.level.name))
}
result.addAll(compilerArgs.args)
result.addAll(compilerArgs.argsSplit())
if (!debug) {
result.add("--")
result.addAll(exeArgs.args)
result.addAll(exeArgs.argsSplit())
}
return result
}

View file

@ -51,7 +51,7 @@ class ZigExecConfigTest(project: Project, factory: ConfigurationFactory): ZigExe
if (!debug || optimization.forced) {
result.addAll(listOf("-O", optimization.level.name))
}
result.addAll(compilerArgs.args)
result.addAll(compilerArgs.argsSplit())
if (debug) {
result.add("--test-no-exec")
}

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
@ -68,7 +68,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) {
@ -161,35 +161,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

@ -87,7 +87,7 @@ class BuildToolWindowContext(private val project: Project): Disposable {
val factory = firstConfigFactory<ZigConfigTypeBuild>()
val newConfig = manager.createConfiguration("zig build $stepName", factory)
val config = newConfig.configuration as ZigExecConfigBuild
config.buildSteps.args = listOf(stepName)
config.buildSteps.args = stepName
manager.addConfiguration(newConfig)
return@run newConfig
}
@ -213,7 +213,7 @@ private fun getViewport(project: Project): JBScrollPane? {
private fun getExistingRunConfig(manager: RunManager, stepName: String): RunnerAndConfigurationSettings? {
for (config in manager.getConfigurationSettingsList(ZigConfigTypeBuild::class.java)) {
val build = config.configuration as? ZigExecConfigBuild ?: continue
val steps = build.buildSteps.args
val steps = build.buildSteps.argsSplit()
if (steps.size != 1)
continue
if (steps[0] != stepName)

View file

@ -131,7 +131,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"
}
@ -155,7 +155,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

@ -23,52 +23,26 @@
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.isRegularFile
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
val (process, exitCode) = withContext(Dispatchers.IO) {
val process = cli.createProcess()
val exit = withTimeoutOrNull(timeoutMillis) {
process.awaitExit()
}
process to exit
}
return runInterruptible {
ProcessOutput(
process.inputStream.bufferedReader().use { it.readText() },
process.errorStream.bufferedReader().use { it.readText() },
exitCode ?: -1,
exitCode == null,
false
)
}
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) } }
return cli.call(timeoutMillis)
}
private suspend fun createBaseCommandLine(
workingDirectory: Path?,
vararg parameters: String
): GeneralCommandLine? {
): Result<GeneralCommandLine> {
val exe = toolchain.pathToExecutable(toolName)
if (!exe.isRegularFile())
return null
val cli = GeneralCommandLine()
.withExePath(exe.toString())
.withWorkingDirectory(workingDirectory)
.withParameters(*parameters)
.withCharset(Charsets.UTF_8)
return toolchain.patchCommandLine(cli)
return createCommandLineSafe(workingDirectory, exe, *parameters)
.mapCatching { toolchain.patchCommandLine(it) }
}
}

View file

@ -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
))
}
}

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

@ -1,7 +1,7 @@
pluginName=ZigBrains
pluginRepositoryUrl=https://github.com/FalsePattern/ZigBrains
pluginVersion=21.1.0
pluginVersion=22.0.0
pluginSinceBuild=242
pluginUntilBuild=242.*

View file

@ -26,8 +26,12 @@ import com.falsepattern.zigbrains.direnv.DirenvCmd
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
import com.falsepattern.zigbrains.lsp.settings.zlsSettings
import com.falsepattern.zigbrains.shared.zigCoroutineScope
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.ui.EditorNotifications
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.io.path.pathString
class ZLSStartup: ProjectActivity {
@ -43,5 +47,16 @@ class ZLSStartup: ProjectActivity {
project.zlsSettings.state = zlsState
}
}
project.zigCoroutineScope.launch {
var currentState = project.zlsRunningAsync()
while (!project.isDisposed) {
val running = project.zlsRunningAsync()
if (currentState != running) {
EditorNotifications.getInstance(project).updateAllNotifications()
}
currentState = running
delay(1000)
}
}
}
}

View file

@ -71,15 +71,43 @@ class ZigLanguageServerFactory: LanguageServerFactory, LanguageServerEnablementS
return features
}
override fun isEnabled(project: Project): Boolean {
return (project.getUserData(ENABLED_KEY) != false) && project.zlsSettings.validate()
}
override fun isEnabled(project: Project) = project.zlsEnabledSync()
override fun setEnabled(enabled: Boolean, project: Project) {
project.putUserData(ENABLED_KEY, enabled)
project.zlsEnabled(enabled)
}
}
suspend fun Project.zlsEnabledAsync(): Boolean {
return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateAsync()
}
fun Project.zlsEnabledSync(): Boolean {
return (getUserData(ENABLED_KEY) != false) && zlsSettings.validateSync()
}
fun Project.zlsEnabled(value: Boolean) {
putUserData(ENABLED_KEY, value)
}
suspend fun Project.zlsRunningAsync(): Boolean {
if (!zlsEnabledAsync())
return false
return zlsRunningLsp4ij()
}
fun Project.zlsRunningSync(): Boolean {
if (!zlsEnabledSync())
return false
return zlsRunningLsp4ij()
}
private fun Project.zlsRunningLsp4ij(): Boolean {
val manager = service<LanguageServerManager>()
val status = manager.getServerStatus("ZigBrains")
return status == ServerStatus.started || status == ServerStatus.starting
}
class ZLSStarter: LanguageServerStarter {
override fun startLSP(project: Project, restart: Boolean) {
project.zigCoroutineScope.launch {
@ -87,7 +115,10 @@ class ZLSStarter: LanguageServerStarter {
val status = manager.getServerStatus("ZigBrains")
if ((status == ServerStatus.started || status == ServerStatus.starting) && !restart)
return@launch
manager.start("ZigBrains")
manager.stop("ZigBrains")
if (project.zlsSettings.validateAsync()) {
manager.start("ZigBrains")
}
}
}

View file

@ -0,0 +1,64 @@
/*
* This file is part of ZigBrains.
*
* Copyright (C) 2023-2025 FalsePattern
* All Rights Reserved
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* ZigBrains is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, only version 3 of the License.
*
* ZigBrains is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with ZigBrains. If not, see <https://www.gnu.org/licenses/>.
*/
package com.falsepattern.zigbrains.lsp.notification
import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.settings.zlsSettings
import com.falsepattern.zigbrains.lsp.zlsRunningSync
import com.falsepattern.zigbrains.zig.ZigFileType
import com.falsepattern.zigbrains.zon.ZonFileType
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.ui.EditorNotificationPanel
import com.intellij.ui.EditorNotificationProvider
import java.util.function.Function
import javax.swing.JComponent
class ZigEditorNotificationProvider: EditorNotificationProvider, DumbAware {
override fun collectNotificationData(
project: Project,
file: VirtualFile
): Function<in FileEditor, out JComponent?>? {
when (file.fileType) {
ZigFileType, ZonFileType -> {}
else -> return null
}
if (project.zlsRunningSync()) {
return null
}
return Function { editor ->
val status: EditorNotificationPanel.Status
val message: String
if (!project.zlsSettings.validateSync()) {
status = EditorNotificationPanel.Status.Error
message = ZLSBundle.message("notification.banner.zls-bad-config")
} else {
status = EditorNotificationPanel.Status.Warning
message = ZLSBundle.message("notification.banner.zls-not-running")
}
EditorNotificationPanel(editor, status).also { it.text = message }
}
}
}

View file

@ -25,6 +25,7 @@ package com.falsepattern.zigbrains.lsp.settings
import com.falsepattern.zigbrains.direnv.emptyEnv
import com.falsepattern.zigbrains.direnv.getDirenv
import com.falsepattern.zigbrains.lsp.ZLSBundle
import com.falsepattern.zigbrains.lsp.startLSP
import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.io.toNioPathOrNull
@ -32,9 +33,9 @@ import com.intellij.platform.ide.progress.ModalTaskOwner
import com.intellij.platform.ide.progress.runWithModalProgressBlocking
import com.intellij.util.application
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.nio.file.Path
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.io.path.isExecutable
import kotlin.io.path.isRegularFile
@ -51,47 +52,45 @@ class ZLSProjectSettingsService(val project: Project): PersistentStateComponent<
@Volatile
private var valid = false
private val mutex = ReentrantLock()
private val mutex = Mutex()
override fun getState(): ZLSSettings {
return state.copy()
}
fun setState(value: ZLSSettings) {
mutex.withLock {
this.state = value
dirty = true
runBlocking {
mutex.withLock {
this@ZLSProjectSettingsService.state = value
dirty = true
}
}
startLSP(project, true)
}
override fun loadState(state: ZLSSettings) {
mutex.withLock {
this.state = state
dirty = true
}
setState(state)
}
fun isModified(otherData: ZLSSettings): Boolean {
return state != otherData
}
fun validate(): Boolean {
suspend fun validateAsync(): Boolean {
mutex.withLock {
if (dirty) {
val state = this.state
valid = if (application.isDispatchThread) {
runWithModalProgressBlocking(ModalTaskOwner.project(project), ZLSBundle.message("progress.title.validate")) {
doValidate(project, state)
}
} else {
runBlocking {
doValidate(project, state)
}
}
valid = doValidate(project, state)
dirty = false
}
return valid
}
}
fun validateSync() = if (application.isDispatchThread) {
runWithModalProgressBlocking(ModalTaskOwner.project(project), ZLSBundle.message("progress.title.validate")) {
validateAsync()
}
} else {
runBlocking {
validateAsync()
}
}
}
private suspend fun doValidate(project: Project, state: ZLSSettings): Boolean {

View file

@ -38,7 +38,7 @@ data class ZLSSettings(
val enable_argument_placeholders: Boolean = true,
val completion_label_details: Boolean = true,
val enable_build_on_save: Boolean = false,
val build_on_save_args: List<String> = emptyList(),
val build_on_save_args: String = "",
val semantic_tokens: SemanticTokens = SemanticTokens.full,
val inlay_hints_show_variable_type_hints: Boolean = true,
val inlay_hints_show_struct_literal_field_type: Boolean = true,

View file

@ -24,6 +24,7 @@ package com.falsepattern.zigbrains.lsp.settings
import com.falsepattern.zigbrains.lsp.config.ZLSConfig
import com.falsepattern.zigbrains.lsp.config.ZLSConfigProvider
import com.falsepattern.zigbrains.shared.cli.translateCommandline
import com.intellij.openapi.project.Project
class ZLSSettingsConfigProvider: ZLSConfigProvider {
@ -34,7 +35,14 @@ class ZLSSettingsConfigProvider: ZLSConfigProvider {
enable_argument_placeholders = state.enable_argument_placeholders,
completion_label_details = state.completion_label_details,
enable_build_on_save = state.enable_build_on_save,
build_on_save_args = state.build_on_save_args,
build_on_save_args = run {
val args = state.build_on_save_args
return@run if (args.isEmpty()) {
emptyList()
} else {
translateCommandline(args).toList()
}
},
semantic_tokens = state.semantic_tokens,
inlay_hints_show_variable_type_hints = state.inlay_hints_show_variable_type_hints,
inlay_hints_show_struct_literal_field_type = state.inlay_hints_show_struct_literal_field_type,

View file

@ -42,11 +42,7 @@ class ZLSSettingsConfigurable(private val project: Project): SubConfigurable {
override fun apply() {
val data = appSettingsComponent?.data ?: return
val settings = project.zlsSettings
val reloadZLS = settings.isModified(data)
settings.state = data
if (reloadZLS) {
startLSP(project, true)
}
}
override fun reset() {

View file

@ -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,
ZLSBundle.message("settings.zls-path.browse.title"),
FileChooserDescriptorFactory.createSingleFileDescriptor(),
).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,
ZLSBundle.message("settings.zls-config-path.browse.title"),
FileChooserDescriptorFactory.createSingleFileDescriptor()
).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"
@ -207,14 +235,7 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
enable_argument_placeholders.isSelected,
completion_label_details.isSelected,
enable_build_on_save.isSelected,
run {
val args = build_on_save_args.text ?: ""
return@run if (args.isEmpty()) {
emptyList()
} else {
translateCommandline(args).toList()
}
},
build_on_save_args.text,
semantic_tokens.item ?: SemanticTokens.full,
inlay_hints_show_variable_type_hints.isSelected,
inlay_hints_show_struct_literal_field_type.isSelected,
@ -240,7 +261,7 @@ class ZLSSettingsPanel(private val project: Project) : ZigProjectConfigurationPr
enable_argument_placeholders.isSelected = value.enable_argument_placeholders
completion_label_details.isSelected = value.completion_label_details
enable_build_on_save.isSelected = value.enable_build_on_save
build_on_save_args.text = value.build_on_save_args.joinToString(separator = " ") { it }
build_on_save_args.text = value.build_on_save_args
semantic_tokens.item = value.semantic_tokens
inlay_hints_show_variable_type_hints.isSelected = value.inlay_hints_show_variable_type_hints
inlay_hints_show_struct_literal_field_type.isSelected = value.inlay_hints_show_struct_literal_field_type
@ -257,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) {
@ -272,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 {
@ -285,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(
@ -294,80 +354,4 @@ private fun Panel.fancyRow(
) = row(ZLSBundle.message(label)) {
contextHelp(ZLSBundle.message(tooltip))
cb()
}
@Throws(Exception::class)
private fun translateCommandline(toProcess: String): List<String> {
if (toProcess.isEmpty()) {
return emptyList()
}
val normal = 0
val inQuote = 1
val inDoubleQuote = 2
val inEscape = 3
var state = normal
var escapeState = normal
val tok = StringTokenizer(toProcess, "\\\"' ", true)
val v = ArrayList<String>()
val current = StringBuilder()
while (tok.hasMoreTokens()) {
val nextTok = tok.nextToken()
when (state) {
inQuote -> if ("'" == nextTok) {
state = normal
} else if ("\\" == nextTok) {
escapeState = inQuote
state = inEscape
} else {
current.append(nextTok)
}
inDoubleQuote -> if ("\"" == nextTok) {
state = normal
} else if ("\\" == nextTok) {
escapeState = inDoubleQuote
state = inEscape
} else {
current.append(nextTok)
}
inEscape -> {
current.append(when(nextTok) {
"n" -> "\n"
"r" -> "\r"
"t" -> "\t"
else -> nextTok
})
state = escapeState
}
else -> if ("'" == nextTok) {
state = inQuote
} else if ("\"" == nextTok) {
state = inDoubleQuote
} else if (" " == nextTok) {
if (current.isNotEmpty()) {
v.add(current.toString())
current.setLength(0)
}
} else if ("\\" == nextTok) {
escapeState = normal
state = inEscape
} else {
current.append(nextTok)
}
}
}
if (current.isNotEmpty()) {
v.add(current.toString())
}
if (state != inQuote && state != inDoubleQuote) {
return v
} else {
throw IllegalArgumentException("unbalanced quotes in $toProcess")
}
}
}

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

View file

@ -45,6 +45,10 @@
/>
<postStartupActivity
implementation="com.falsepattern.zigbrains.lsp.ZLSStartup"/>
<editorNotificationProvider
implementation="com.falsepattern.zigbrains.lsp.notification.ZigEditorNotificationProvider"
/>
</extensions>
<extensions defaultExtensionNs="com.falsepattern.zigbrains">

View file

@ -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)
@ -59,6 +60,8 @@ notification.message.zls-config-not-exists.content=ZLS config file does not exis
notification.message.zls-config-not-file.content=ZLS config file is not a regular file: {0}
notification.message.zls-config-path-invalid.content=ZLS config path could not be parted: {0}
notification.message.zls-config-autogen-failed.content=Failed to autogenerate ZLS config from toolchain
notification.banner.zls-not-running=Zig Language Server is not running. Check the [Language Servers] tool menu!
notification.banner.zls-bad-config=Zig Language Server is misconfigured. Check [Settings | Languages \\& Frameworks | Zig]!
progress.title.create-connection-provider=Creating ZLS connection provider
progress.title.validate=Validating ZLS
# suppress inspection "UnusedProperty"

View file

@ -5,7 +5,7 @@
<depends config-file="zigbrains-core.xml">com.intellij.modules.platform</depends>
<depends config-file="zigbrains-lsp.xml">com.redhat.devtools.lsp4ij</depends>
<depends config-file="zigbrains-debugger.xml" optional="true">com.intellij.nativeDebug</depends>
<depends config-file="zigbrains-debugger.xml" optional="true">com.intellij.modules.cidr.debugger</depends>
<depends config-file="zigbrains-cidr.xml" optional="true">com.intellij.cidr.base</depends>
<depends config-file="zigbrains-clion.xml" optional="true">com.intellij.clion</depends>