Skip to content

Commit

Permalink
allow intellij project to work with different biome configs (#53)
Browse files Browse the repository at this point in the history
* allow intellij project to work with different biome configs

* add comment

* add information about automatic mode in readme
  • Loading branch information
APshenkin committed May 1, 2024
1 parent 497e091 commit 22395cb
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 53 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
55 changes: 35 additions & 20 deletions src/main/kotlin/com/github/biomejs/intellijbiome/BiomePackage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -17,44 +18,47 @@ 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)
val configurationMode = settings.configurationMode
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()) {
Expand All @@ -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<String>): 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ class BiomeStdinRunner(private val project: Project) : BiomeRunner {
}

override fun createCommandLine(file: VirtualFile, action: String, args: List<String>): 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)

Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Feature> = 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<Document>) {
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)
}
}
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<BiomeServerService>()
// 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,53 @@ 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(
project: Project,
file: VirtualFile,
serverStarter: LspServerSupportProvider.LspServerStarter
) {
val executable = BiomePackage(project).binaryPath() ?: return
serverStarter.ensureServerStarted(LspServerDescriptor(project, executable))
val currentConfigPath = project.service<BiomeServerService>().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<BiomeServerService>().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)

Expand All @@ -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()) {
Expand All @@ -59,6 +81,7 @@ private class LspServerDescriptor(project: Project, val executable: String) :

return GeneralCommandLine().runBiomeCLI(project, executable).apply {
addParameters(params)
withWorkDirectory(configPath)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -57,6 +59,20 @@ class BiomeSettings :
state.applyUnsafeFixesOnSave = value
}

fun getEnabledFeatures(): EnumSet<Feature> {
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
}
Expand Down

0 comments on commit 22395cb

Please sign in to comment.