diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e05e4a..c07b71b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ Changelog structure reference: ## [Unreleased] +### Added + +- LSP + - Error/Warning banner at the top of the editor when ZLS is misconfigured/not running + ### Fixed - Debugging diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt index 6c79451d..f126be57 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZLSStartup.kt @@ -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) + } + } } } \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt index 23a78eac..d1c61d49 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/ZigLanguageServerFactory.kt @@ -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() + 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") + } } } diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt new file mode 100644 index 00000000..b72dde64 --- /dev/null +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/notification/ZigEditorNotificationProvider.kt @@ -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 . + */ + +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? { + 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 } + } + } +} \ No newline at end of file diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt index 618385da..cccda858 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSProjectSettingsService.kt @@ -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 { diff --git a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt index d5ebe2c5..edb49349 100644 --- a/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt +++ b/lsp/src/main/kotlin/com/falsepattern/zigbrains/lsp/settings/ZLSSettingsConfigurable.kt @@ -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() { diff --git a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml index 5a749c2c..a9c5f872 100644 --- a/lsp/src/main/resources/META-INF/zigbrains-lsp.xml +++ b/lsp/src/main/resources/META-INF/zigbrains-lsp.xml @@ -45,6 +45,10 @@ /> + + diff --git a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties index a23813e7..479fd925 100644 --- a/lsp/src/main/resources/zigbrains/lsp/Bundle.properties +++ b/lsp/src/main/resources/zigbrains/lsp/Bundle.properties @@ -59,6 +59,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"