feat!: LSP misconfiguration warning banner

This commit is contained in:
FalsePattern 2025-03-13 13:52:06 +01:00
parent 03a86defb9
commit 05ff125c1d
Signed by: falsepattern
GPG key ID: E930CDEC50C50E23
8 changed files with 149 additions and 33 deletions

View file

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

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,9 +115,12 @@ class ZLSStarter: LanguageServerStarter {
val status = manager.getServerStatus("ZigBrains")
if ((status == ServerStatus.started || status == ServerStatus.starting) && !restart)
return@launch
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) {
runBlocking {
mutex.withLock {
this.state = value
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

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

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

@ -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"