From 22395cbd005a563a387bbc99f4ca349ef81a8c8f Mon Sep 17 00:00:00 2001 From: Andrey Pshenkin Date: Wed, 1 May 2024 08:54:54 +0100 Subject: [PATCH] allow intellij project to work with different biome configs (#53) * allow intellij project to work with different biome configs * add comment * add information about automatic mode in readme --- README.md | 7 +++ .../biomejs/intellijbiome/BiomePackage.kt | 55 ++++++++++++------- .../biomejs/intellijbiome/BiomeStdinRunner.kt | 5 +- .../actions/BiomeCheckOnSaveAction.kt | 24 +------- .../actions/ReformatWithBiomeAction.kt | 11 +++- .../listeners/BiomeEditorPanelListener.kt | 41 ++++++++++++++ .../lsp/BiomeLspServerSupportProvider.kt | 35 ++++++++++-- .../services/BiomeServerService.kt | 21 +++++++ .../intellijbiome/settings/BiomeSettings.kt | 16 ++++++ 9 files changed, 162 insertions(+), 53 deletions(-) create mode 100644 src/main/kotlin/com/github/biomejs/intellijbiome/listeners/BiomeEditorPanelListener.kt diff --git a/README.md b/README.md index 2160f86..9432cb4 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ The plugin tries to use Biome from your project’s local dependencies (`node_mo You can also explicitly specify the `biome` binary the extension should use by configuring the `Biome CLI Path` in `Settings`->`Language & Frameworks`->`Biome Settings`. +### Biome Config resolution +In `Automatic Biome configuration` mode, the plugin will look for a biome configuration file upwards from the current file. If it doesn't find one, it will stop LSP server. +There are several reasons to behave like this: +1. In IDEA with multiple projects in one code base it sound reasonable to disable LSP server if there is no biome configuration file in the project. +2. In multi-root workspace, we should run biome from proper working directory, so that `include` and `exclude` paths are resolved correctly. +3. As currently LSP server proxy doesn't provide option to work with multiple configs, we should provide path to the biome configuration file and restart LSP server once active file is changed. + ### Plugin settings #### `Biome CLI Path` diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomePackage.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomePackage.kt index 4220af1..beaced2 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomePackage.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomePackage.kt @@ -8,6 +8,7 @@ import com.intellij.execution.util.ExecUtil import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager import com.intellij.javascript.nodejs.util.NodePackage import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile import java.nio.file.Paths class BiomePackage(private val project: Project) { @@ -17,17 +18,15 @@ class BiomePackage(private val project: Project) { return NodePackage.findDefaultPackage(project, "@biomejs/biome", interpreter) } - val configPath: String? - get() { - val settings = BiomeSettings.getInstance(project) - val configurationMode = settings.configurationMode - - return when (configurationMode) { - ConfigurationMode.DISABLED -> null - ConfigurationMode.AUTOMATIC -> null - ConfigurationMode.MANUAL -> BiomeSettings.getInstance(project).configPath - } + fun configPath(file: VirtualFile): String? { + val settings = BiomeSettings.getInstance(project) + val configurationMode = settings.configurationMode + return when (configurationMode) { + ConfigurationMode.DISABLED -> null + ConfigurationMode.AUTOMATIC -> findPathUpwards(file, configValidExtensions.map { "$configName.$it" })?.path + ConfigurationMode.MANUAL -> settings.configPath } + } fun versionNumber(): String? { val settings = BiomeSettings.getInstance(project) @@ -35,26 +34,31 @@ class BiomePackage(private val project: Project) { return when (configurationMode) { ConfigurationMode.DISABLED -> null ConfigurationMode.AUTOMATIC -> nodePackage?.getVersion(project)?.toString() - ConfigurationMode.MANUAL -> getBinaryVersion(binaryPath()) + ConfigurationMode.MANUAL -> getBinaryVersion(binaryPath(null, true)) } } - fun binaryPath(): String? { + fun binaryPath(configPath: String?, showVersion: Boolean): String? { val settings = BiomeSettings.getInstance(project) val configurationMode = settings.configurationMode return when (configurationMode) { ConfigurationMode.DISABLED -> null - ConfigurationMode.AUTOMATIC -> nodePackage?.getAbsolutePackagePathToRequire(project)?.let { - Paths.get( - it, - "bin/biome" - ) - }?.toString() - - ConfigurationMode.MANUAL -> settings.executablePath + // don't try to find the executable path if the configuration file does not exist. + // This will prevent start LSP and formatting in case if biome is not used in the project. + ConfigurationMode.AUTOMATIC -> if (configPath != null || showVersion) findBiomeExecutable() else null + // if configuration mode is manual, return the executable path if it is not empty string. + // Otherwise, try to find the executable path. + ConfigurationMode.MANUAL -> if (settings.executablePath == "") findBiomeExecutable() else settings.executablePath } } + private fun findBiomeExecutable() = nodePackage?.getAbsolutePackagePathToRequire(project)?.let { + Paths.get( + it, + "bin/biome" + ) + }?.toString() + private fun getBinaryVersion(binaryPath: String?): String? { if (binaryPath.isNullOrEmpty()) { @@ -77,4 +81,15 @@ class BiomePackage(private val project: Project) { const val configName = "biome" val configValidExtensions = listOf("json", "jsonc") } + + private fun findPathUpwards(file: VirtualFile, fileName: List): VirtualFile? { + var cur = file.parent + while (cur != null) { + if (cur.children.find { name -> fileName.any { it == name.name } } != null) { + return cur + } + cur = cur.parent + } + return null + } } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeStdinRunner.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeStdinRunner.kt index 6df4434..ace92d1 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeStdinRunner.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeStdinRunner.kt @@ -48,8 +48,8 @@ class BiomeStdinRunner(private val project: Project) : BiomeRunner { } override fun createCommandLine(file: VirtualFile, action: String, args: List): GeneralCommandLine { - val configPath = biomePackage.configPath - val exePath = biomePackage.binaryPath() + val configPath = biomePackage.configPath(file) + val exePath = biomePackage.binaryPath(configPath, false) val params = SmartList(action, "--stdin-file-path", file.path) params.addAll(args) @@ -64,6 +64,7 @@ class BiomeStdinRunner(private val project: Project) : BiomeRunner { return GeneralCommandLine().runBiomeCLI(project, exePath).apply { withInput(File(file.path)) + withWorkDirectory(configPath) addParameters(params) } } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/BiomeCheckOnSaveAction.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/BiomeCheckOnSaveAction.kt index 028eb4d..4b51abb 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/BiomeCheckOnSaveAction.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/BiomeCheckOnSaveAction.kt @@ -1,41 +1,21 @@ package com.github.biomejs.intellijbiome.actions -import com.github.biomejs.intellijbiome.Feature import com.github.biomejs.intellijbiome.settings.BiomeSettings import com.intellij.ide.actionsOnSave.impl.ActionsOnSaveFileDocumentManagerListener import com.intellij.openapi.editor.Document import com.intellij.openapi.project.Project -import java.util.* class BiomeCheckOnSaveAction() : ActionsOnSaveFileDocumentManagerListener.ActionOnSave() { - private var features: EnumSet = EnumSet.noneOf(Feature::class.java) override fun isEnabledForProject(project: Project): Boolean { val settings = BiomeSettings.getInstance(project) - setFeatures(settings) - return settings.formatOnSave || settings.applySafeFixesOnSave || settings.applyUnsafeFixesOnSave } override fun processDocuments(project: Project, documents: Array) { - BiomeCheckRunner().run(project, features, documents) - } - - private fun setFeatures(settings: BiomeSettings) { - features = EnumSet.noneOf(Feature::class.java) - - if (settings.formatOnSave) { - features.add(Feature.Format) - } - - if (settings.applySafeFixesOnSave) { - features.add(Feature.SafeFixes) - } - - if (settings.applyUnsafeFixesOnSave) { - features.add(Feature.UnsafeFixes) - } + val settings = BiomeSettings.getInstance(project) + BiomeCheckRunner().run(project, settings.getEnabledFeatures(), documents) } } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ReformatWithBiomeAction.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ReformatWithBiomeAction.kt index 3f4ecca..aa6c43a 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ReformatWithBiomeAction.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ReformatWithBiomeAction.kt @@ -1,14 +1,13 @@ package com.github.biomejs.intellijbiome.actions -import com.github.biomejs.intellijbiome.Feature import com.github.biomejs.intellijbiome.settings.BiomeSettings import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.project.DumbAware -import java.util.* class ReformatWithBiomeAction : AnAction(), DumbAware { @@ -19,7 +18,13 @@ class ReformatWithBiomeAction : AnAction(), DumbAware { val editor: Editor? = actionEvent.getData(CommonDataKeys.EDITOR) if (editor != null) { - BiomeCheckRunner().run(project, EnumSet.of(Feature.Format), arrayOf(editor.document)) + val documentManager = FileDocumentManager.getInstance() + // We should save document before running Biome, because Biome will read the file from disk and user changes can be lost + if (documentManager.isDocumentUnsaved(editor.document)) { + documentManager.saveDocument(editor.document) + } + val settings = BiomeSettings.getInstance(project) + BiomeCheckRunner().run(project, settings.getEnabledFeatures(), arrayOf(editor.document)) } } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/listeners/BiomeEditorPanelListener.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/listeners/BiomeEditorPanelListener.kt new file mode 100644 index 0000000..67abb69 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/listeners/BiomeEditorPanelListener.kt @@ -0,0 +1,41 @@ +package com.github.biomejs.intellijbiome.listeners + +import com.github.biomejs.intellijbiome.BiomePackage +import com.github.biomejs.intellijbiome.services.BiomeServerService +import com.github.biomejs.intellijbiome.settings.BiomeSettings +import com.intellij.openapi.components.service +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.project.Project + +// This implements a listener for file editor manager events. +// It listens for file selection changes in IDE and restarts LSP server if selected file in editor should be checked with different Biome config. +class BiomeEditorPanelListener(private val project: Project) : FileEditorManagerListener { + + private var currentConfigPath: String? = null + + // on selection change, check if the new file should not use different biome config. + // if so, restart biome server to use the new config. + override fun selectionChanged(fileEditorManagerEvent: FileEditorManagerEvent) { + val settings = BiomeSettings.getInstance(project) + val isEnabled = settings.isEnabled() + if (fileEditorManagerEvent.newFile != null) { + val newConfigPath = BiomePackage(project).configPath(fileEditorManagerEvent.newFile!!) + val biomeServerService = project.service() + // stop biome LSP server if selected file does not have biome config. + if (newConfigPath == null) { + currentConfigPath = null + biomeServerService.stopBiomeServer() + return + } + if (isEnabled && currentConfigPath != newConfigPath) { + currentConfigPath = newConfigPath + biomeServerService.restartBiomeServer() + } + } + } + + fun getCurrentConfigPath(): String? { + return currentConfigPath + } +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/lsp/BiomeLspServerSupportProvider.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/lsp/BiomeLspServerSupportProvider.kt index f2676bf..9c3fe2b 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/lsp/BiomeLspServerSupportProvider.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/lsp/BiomeLspServerSupportProvider.kt @@ -4,16 +4,18 @@ import com.github.biomejs.intellijbiome.BiomeBundle import com.github.biomejs.intellijbiome.BiomePackage import com.github.biomejs.intellijbiome.extensions.runBiomeCLI import com.github.biomejs.intellijbiome.listeners.BIOME_CONFIG_RESOLVED_TOPIC +import com.github.biomejs.intellijbiome.services.BiomeServerService import com.github.biomejs.intellijbiome.settings.BiomeSettings import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile -import com.intellij.platform.lsp.api.LspServerSupportProvider -import com.intellij.platform.lsp.api.ProjectWideLspServerDescriptor +import com.intellij.platform.lsp.api.* import com.intellij.platform.lsp.api.customization.LspFormattingSupport import com.intellij.util.SmartList + @Suppress("UnstableApiUsage") class BiomeLspServerSupportProvider : LspServerSupportProvider { override fun fileOpened( @@ -21,13 +23,34 @@ class BiomeLspServerSupportProvider : LspServerSupportProvider { file: VirtualFile, serverStarter: LspServerSupportProvider.LspServerStarter ) { - val executable = BiomePackage(project).binaryPath() ?: return - serverStarter.ensureServerStarted(LspServerDescriptor(project, executable)) + val currentConfigPath = project.service().getCurrentConfigPath() + if (currentConfigPath != null) { + val executable = BiomePackage(project).binaryPath(currentConfigPath, false) ?: return + serverStarter.ensureServerStarted(BiomeLspServerDescriptor(project, executable, currentConfigPath)) + return + } + + val configPath = BiomePackage(project).configPath(file) + val executable = BiomePackage(project).binaryPath(configPath, false) ?: return + serverStarter.ensureServerStarted(BiomeLspServerDescriptor(project, executable, configPath)) + } +} + +@Suppress("UnstableApiUsage") +class BiomeLspServerManagerListener(val project: Project) : LspServerManagerListener { + override fun serverStateChanged(lspServer: LspServer) { + if (lspServer.descriptor is BiomeLspServerDescriptor && lspServer.state == LspServerState.ShutdownUnexpectedly) { + // restart again if the server was shutdown unexpectedly. + // This can be caused by race condition, when we restart LSP server because of config change, + // but Intellij also tried to send a request to it at the same time. + // Unfortunate There is no way prevent IDEA send requests after LSP started. + project.service().restartBiomeServer() + } } } @Suppress("UnstableApiUsage") -private class LspServerDescriptor(project: Project, val executable: String) : +private class BiomeLspServerDescriptor(project: Project, val executable: String, val configPath: String?) : ProjectWideLspServerDescriptor(project, "Biome") { private val biomePackage = BiomePackage(project) @@ -41,7 +64,6 @@ private class LspServerDescriptor(project: Project, val executable: String) : } override fun createCommandLine(): GeneralCommandLine { - val configPath = biomePackage.configPath val params = SmartList("lsp-proxy") if (!configPath.isNullOrEmpty()) { @@ -59,6 +81,7 @@ private class LspServerDescriptor(project: Project, val executable: String) : return GeneralCommandLine().runBiomeCLI(project, executable).apply { addParameters(params) + withWorkDirectory(configPath) } } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/services/BiomeServerService.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/services/BiomeServerService.kt index 47b6596..659e1f0 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/services/BiomeServerService.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/services/BiomeServerService.kt @@ -1,19 +1,40 @@ package com.github.biomejs.intellijbiome.services import com.github.biomejs.intellijbiome.BiomeBundle +import com.github.biomejs.intellijbiome.listeners.BiomeEditorPanelListener +import com.github.biomejs.intellijbiome.lsp.BiomeLspServerManagerListener import com.github.biomejs.intellijbiome.lsp.BiomeLspServerSupportProvider import com.intellij.notification.NotificationGroupManager import com.intellij.notification.NotificationType import com.intellij.openapi.components.Service +import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Disposer import com.intellij.platform.lsp.api.LspServerManager @Service(Service.Level.PROJECT) class BiomeServerService(private val project: Project) { + private val editorPanelListener: BiomeEditorPanelListener + + init { + addBiomeLspListener() + editorPanelListener = BiomeEditorPanelListener(project) + project.messageBus.connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, editorPanelListener) + } + + fun getCurrentConfigPath(): String? { + return editorPanelListener.getCurrentConfigPath() + } + fun restartBiomeServer() { LspServerManager.getInstance(project).stopAndRestartIfNeeded(BiomeLspServerSupportProvider::class.java) } + fun addBiomeLspListener() { + LspServerManager.getInstance(project) + .addLspServerManagerListener(BiomeLspServerManagerListener(project), Disposer.newDisposable(), true) + } + fun stopBiomeServer() { LspServerManager.getInstance(project).stopServers(BiomeLspServerSupportProvider::class.java) } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettings.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettings.kt index b37524f..4bca652 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettings.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettings.kt @@ -1,10 +1,12 @@ package com.github.biomejs.intellijbiome.settings +import com.github.biomejs.intellijbiome.Feature import com.intellij.lang.javascript.linter.GlobPatternUtil import com.intellij.openapi.components.* import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import java.io.File +import java.util.* @Service(Service.Level.PROJECT) @@ -57,6 +59,20 @@ class BiomeSettings : state.applyUnsafeFixesOnSave = value } + fun getEnabledFeatures(): EnumSet { + val features = EnumSet.noneOf(Feature::class.java) + if (formatOnSave) { + features.add(Feature.Format) + } + if (applySafeFixesOnSave) { + features.add(Feature.SafeFixes) + } + if (applyUnsafeFixesOnSave) { + features.add(Feature.UnsafeFixes) + } + return features + } + fun isEnabled(): Boolean { return configurationMode !== ConfigurationMode.DISABLED }