diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9f51f5b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 2 + +[*.kt] +indent_size = 4 diff --git a/.github/workflows/integrate.yaml b/.github/workflows/integrate.yaml index a3ffb03..351a6f3 100644 --- a/.github/workflows/integrate.yaml +++ b/.github/workflows/integrate.yaml @@ -2,11 +2,11 @@ name: Integrate on: push: - branches: ["main"] + branches: [ "main" ] pull_request: workflow_dispatch: jobs: build: name: Build - uses: ./.github/workflows/_build.yaml \ No newline at end of file + uses: ./.github/workflows/_build.yaml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index b8203c3..d5b761a 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -7,7 +7,7 @@ on: type: boolean default: true required: false - description: 'Publish a nightly build' + description: 'Publish a nightly build' jobs: build: @@ -16,10 +16,10 @@ jobs: with: nightly: ${{ inputs.nightly }} artifact: true - + publish-jetbrains: name: Publish to JetBrains Marketplace - needs: [build] + needs: [ build ] runs-on: ubuntu-latest environment: intellij-plugin steps: @@ -32,7 +32,7 @@ jobs: uses: actions/download-artifact@v4 with: name: Biome-${{ needs.build.outputs.version }}.zip - + - name: Restore gradle.properties uses: actions/cache/restore@v3 with: @@ -49,7 +49,7 @@ jobs: publish-github-release: name: Publish to GitHub Releases - needs: [build] + needs: [ build ] runs-on: ubuntu-latest permissions: contents: write @@ -87,4 +87,4 @@ jobs: prerelease: ${{ needs.build.outputs.nightly == 'true' }} draft: true files: biome.zip - tag_name: ${{ needs.build.outputs.nightly == 'true' && github.ref || format('v{0}', needs.build.outputs.version) }} \ No newline at end of file + tag_name: ${{ needs.build.outputs.nightly == 'true' && github.ref || format('v{0}', needs.build.outputs.version) }} diff --git a/.gitignore b/.gitignore index e2e5d94..4a170d1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .idea .qodana build +.DS_Store +node_modules diff --git a/.run/Run IDE for UI Tests.run.xml b/.run/Run IDE for UI Tests.run.xml index ee99b7e..55343a1 100644 --- a/.run/Run IDE for UI Tests.run.xml +++ b/.run/Run IDE for UI Tests.run.xml @@ -1,25 +1,25 @@ - + - - true true false false - + - \ No newline at end of file + diff --git a/.run/Run Plugin.run.xml b/.run/Run Plugin.run.xml index d15ff68..03427a3 100644 --- a/.run/Run Plugin.run.xml +++ b/.run/Run Plugin.run.xml @@ -1,24 +1,24 @@ - + - - true true false - + - \ No newline at end of file + diff --git a/.run/Run Tests.run.xml b/.run/Run Tests.run.xml index 132d9ad..6458c84 100644 --- a/.run/Run Tests.run.xml +++ b/.run/Run Tests.run.xml @@ -1,24 +1,24 @@ - + - - true true false - + diff --git a/.run/Run Verifications.run.xml b/.run/Run Verifications.run.xml index 3a8d688..bf8bccc 100644 --- a/.run/Run Verifications.run.xml +++ b/.run/Run Verifications.run.xml @@ -1,26 +1,27 @@ - + - - true true false - - \ No newline at end of file + diff --git a/README.md b/README.md index b304fa5..4a176bd 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,12 @@ +[Biome](https://biomejs.dev/) is a powerful tool designed to enhance your development experience. This plugin integrates +seamlessly with many [JetBrains IDE's](#Supported IDEs) to provide some capabilities: -[Biome](https://biomejs.dev/) is a powerful tool designed to enhance your development experience. This plugin integrates seamlessly with many [JetBrains IDE's](#Supported IDEs) to provide some capabilities: - -- See lints while you type -- Apply code fixes (from mouse-over, + or Alt+Enter) -- Reformat your code with ⌥⇧++L or Ctrl+Alt+L (You can also format your [code on save](https://www.jetbrains.com/help/webstorm/reformat-and-rearrange-code.html#reformat-on-save)) +- See lints while you type +- Apply code fixes (from mouse-over,+or + Alt+Enter) +- Reformat your code with⌥⇧++Lor + Ctrl+Alt+L (You can also format + your [code on save](https://www.jetbrains.com/help/webstorm/reformat-and-rearrange-code.html#reformat-on-save)) However, please note the following limitations: @@ -11,7 +14,8 @@ However, please note the following limitations: ## Installation -To install the Biome IntelliJ Plugin, Head over to [official plugin page](https://plugins.jetbrains.com/plugin/22761-biome) or follow these steps: +To install the Biome IntelliJ Plugin, Head over +to [official plugin page](https://plugins.jetbrains.com/plugin/22761-biome) or follow these steps: ### From JetBrains IDEs @@ -30,9 +34,11 @@ To install the Biome IntelliJ Plugin, Head over to [official plugin page](https: ## Biome Resolution -The Plugin tries to use Biome from your project’s local dependencies (`node_modules/.bin/biome`). We recommend adding Biome as a project dependency to ensure that NPM scripts and the extension use the same Biome version. +The Plugin tries to use Biome from your project’s local dependencies (`node_modules/.bin/biome`). We recommend adding +Biome as a project dependency to ensure that NPM scripts and the extension use the same Biome version. -You can also explicitly specify the `Biome` binary the extension should use by configuring the `Biome CLI Path` in `Settings`->`Language & Frameworks`->`Biome Settings`. +You can also explicitly specify the`Biome`binary the extension should use by configuring the`Biome CLI Path` +in `Settings`->`Language & Frameworks`->`Biome Settings`. ## Plugin settings @@ -44,7 +50,7 @@ This setting overrides the Biome binary used by the plugin. This plugin is currently supported in the following IDEs: -- IntelliJ IDEA Ultimate >2023.2.2 +- IntelliJ IDEA Ultimate >2023.2.2 - WebStorm >2023.2.2 - AppCode >2023.2.2 - PhpStorm >2023.2.2 @@ -52,4 +58,7 @@ This plugin is currently supported in the following IDEs: ## Contributing -We welcome contributions to the Biome IntelliJ Plugin. If you encounter any issues or have suggestions for improvements, please open an issue on our [GitHub repository](https://github.com/biomejs/biome/issues/new/choose). We also have a [Discord community](https://discord.gg/BypW39g6Yc) where you can discuss the plugin, ask questions, and connect with other Biome's developers. +We welcome contributions to the Biome IntelliJ Plugin. If you encounter any issues or have suggestions for improvements, +please open an issue on our [GitHub repository](https://github.com/biomejs/biome/issues/new/choose). We also have +a [Discord community](https://discord.gg/BypW39g6Yc) where you can discuss the plugin, ask questions, and connect with +other Biome's developers. diff --git a/build.gradle.kts b/build.gradle.kts index e8b9d48..65a3240 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,12 +2,12 @@ import java.net.URI fun properties(key: String) = providers.gradleProperty(key) fun environment(key: String) = providers.environmentVariable(key) -val remoteRobotVersion = "0.11.20" +val remoteRobotVersion = "0.11.21" plugins { - id("java") // Java support - alias(libs.plugins.kotlin) // Kotlin support - alias(libs.plugins.gradleIntelliJPlugin) // Gradle IntelliJ Plugin + id("java") // Java support + alias(libs.plugins.kotlin) // Kotlin support + alias(libs.plugins.gradleIntelliJPlugin) // Gradle IntelliJ Plugin } group = properties("pluginGroup").get() @@ -15,88 +15,85 @@ version = properties("pluginVersion").get() // Configure project's dependencies repositories { - mavenCentral() - maven { url = URI("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") } + mavenCentral() + maven { url = URI("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies") } } // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog dependencies { - testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") - testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion") - testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") - testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.9.3") - - // Logging Network Calls - testImplementation("com.squareup.okhttp3:logging-interceptor:4.11.0") - - // Video Recording - implementation("com.automation-remarks:video-recorder-junit5:2.0") + testImplementation("com.intellij.remoterobot:remote-robot:$remoteRobotVersion") + testImplementation("com.intellij.remoterobot:remote-fixtures:$remoteRobotVersion") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.0") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.9.3") + + // Logging Network Calls + testImplementation("com.squareup.okhttp3:logging-interceptor:4.12.0") } // Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+. kotlin { - jvmToolchain(17) + jvmToolchain(17) } // Configure Gradle IntelliJ Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html intellij { - pluginName = properties("pluginName") - version = properties("platformVersion") - type = properties("platformType") + pluginName = properties("pluginName") + version = properties("platformVersion") + type = properties("platformType") - // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. - plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) } + // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. + plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) } } tasks { - wrapper { - gradleVersion = properties("gradleVersion").get() - } - - patchPluginXml { - version = properties("pluginVersion") - sinceBuild = properties("pluginSinceBuild") - untilBuild = properties("pluginUntilBuild") - } - - downloadRobotServerPlugin { - version.set(remoteRobotVersion) - } - - // Configure UI tests plugin - // Read more: https://github.com/JetBrains/intellij-ui-test-robot - runIdeForUiTests { - systemProperty("robot-server.port", "8082") - systemProperty("ide.mac.message.dialogs.as.sheets", "false") - systemProperty("jb.privacy.policy.text", "") - systemProperty("jb.consents.confirmation.enabled", "false") - systemProperty("ide.mac.file.chooser.native", "false") - systemProperty("jbScreenMenuBar.enabled", "false") - systemProperty("apple.laf.useScreenMenuBar", "false") - systemProperty("idea.trust.all.projects", "true") - systemProperty("ide.show.tips.on.startup.default.value", "false") - systemProperty("eap.require.license", "false") - - } - - test { - useJUnitPlatform() - } - - signPlugin { - certificateChain = environment("CERTIFICATE_CHAIN") - privateKey = environment("PRIVATE_KEY") - password = environment("PRIVATE_KEY_PASSWORD") - } - - publishPlugin { - token = environment("PUBLISH_TOKEN") - // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 - // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: - // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel - channels = properties("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) } - } + wrapper { + gradleVersion = properties("gradleVersion").get() + } + + patchPluginXml { + version = properties("pluginVersion") + sinceBuild = properties("pluginSinceBuild") + untilBuild = properties("pluginUntilBuild") + } + + downloadRobotServerPlugin { + version.set(remoteRobotVersion) + } + + // Configure UI tests plugin + // Read more: https://github.com/JetBrains/intellij-ui-test-robot + runIdeForUiTests { + systemProperty("robot-server.port", "8082") + systemProperty("ide.mac.message.dialogs.as.sheets", "false") + systemProperty("jb.privacy.policy.text", "") + systemProperty("jb.consents.confirmation.enabled", "false") + systemProperty("ide.mac.file.chooser.native", "false") + systemProperty("jbScreenMenuBar.enabled", "false") + systemProperty("apple.laf.useScreenMenuBar", "false") + systemProperty("idea.trust.all.projects", "true") + systemProperty("ide.show.tips.on.startup.default.value", "false") + systemProperty("eap.require.license", "false") + + } + + test { + useJUnitPlatform() + } + + signPlugin { + certificateChain = environment("CERTIFICATE_CHAIN") + privateKey = environment("PRIVATE_KEY") + password = environment("PRIVATE_KEY_PASSWORD") + } + + publishPlugin { + token = environment("PUBLISH_TOKEN") + // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 + // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: + // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel + channels = properties("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) } + } } diff --git a/cliff.toml b/cliff.toml index 40ada13..5761168 100644 --- a/cliff.toml +++ b/cliff.toml @@ -34,15 +34,15 @@ filter_unconventional = true filter_commits = true commit_preprocessors = [ - { pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/biomejs/biome-intellij/pull/${1}))"} + { pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/biomejs/biome-intellij/pull/${1}))" } ] # Commit parsers to use for parsing commits commit_parsers = [ - { message = "^feat", group = "Features" }, - { message = "^fix", group = "Bug Fixes" }, - { message = "^doc", group = "Documentation"}, + { message = "^feat", group = "Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^doc", group = "Documentation" }, ] # Regex for matching git tags -tag_pattern = "v[0-9].*" \ No newline at end of file +tag_pattern = "v[0-9].*" diff --git a/gradle.properties b/gradle.properties index a7511df..b5ef2d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,33 +1,24 @@ # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html - -pluginGroup = com.github.biomejs.intellijbiome -pluginName = Biome -pluginRepositoryUrl = https://github.com/biomejs/biome +pluginGroup=com.github.biomejs.intellijbiome +pluginName=Biome +pluginRepositoryUrl=https://github.com/biomejs/biome # SemVer format -> https://semver.org -pluginVersion = 0.0.7 - +pluginVersion=0.0.7 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html -pluginSinceBuild = 232 - +pluginSinceBuild=233 # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension -platformType = IU -platformVersion = 233-EAP-SNAPSHOT - +platformType=IU +platformVersion=233-EAP-SNAPSHOT # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 platformPlugins=JavaScript - # Gradle Releases -> https://github.com/gradle/gradle/releases -gradleVersion = 8.2.1 - +gradleVersion=8.5 # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib -kotlin.stdlib.default.dependency = false - +kotlin.stdlib.default.dependency=false # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html -org.gradle.configuration-cache = true - +org.gradle.configuration-cache=true # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html -org.gradle.caching = true - +org.gradle.caching=true # Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment -systemProp.org.gradle.unsafe.kotlin.assignment = true +systemProp.org.gradle.unsafe.kotlin.assignment=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99297ef..06c073a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] # libraries -annotations = "24.0.1" +annotations = "24.1.0" # plugins -kotlin = "1.9.0" -gradleIntelliJPlugin = "1.15.0" +kotlin = "1.9.21" +gradleIntelliJPlugin = "1.16.1" [libraries] annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomePackage.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomePackage.kt new file mode 100644 index 0000000..2b69de7 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomePackage.kt @@ -0,0 +1,52 @@ +package com.github.biomejs.intellijbiome + +import com.github.biomejs.intellijbiome.extensions.runWithNodeInterpreter +import com.github.biomejs.intellijbiome.settings.BiomeSettings +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.util.ExecUtil +import com.intellij.javascript.nodejs.library.node_modules.NodeModulesDirectoryManager +import com.intellij.openapi.project.Project + +object BiomePackage { + fun versionNumber(project: Project, binaryPath: String?): String? { + if (binaryPath.isNullOrEmpty()) { + return null + } + + val versionRegex = Regex("\\d{1,2}\\.\\d{1,2}\\.\\d{1,3}") + val commandLine = GeneralCommandLine().runWithNodeInterpreter(project, binaryPath).apply { + addParameter("--version") + } + + val output = ExecUtil.execAndGetOutput(commandLine) + val matchResult = versionRegex.find(output.stdout) + return matchResult?.value + } + + fun binaryPath(project: Project): String? { + val directoryManager = NodeModulesDirectoryManager.getInstance(project) + val executablePath = BiomeSettings.getInstance(project).executablePath + + if (executablePath.isNotEmpty()) { + return executablePath + } + + val binaryFile = directoryManager.nodeModulesDirs + .asSequence() + .mapNotNull { it.findFileByRelativePath("@biomejs/biome/bin/biome") } + .filter { it.isValid } + .firstOrNull() + + return binaryFile?.path + } + + fun configPath(project: Project): String? { + val configPath = BiomeSettings.getInstance(project).configPath + + if (configPath.isNotEmpty()) { + return configPath + } + + return null + } +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeRunner.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeRunner.kt new file mode 100644 index 0000000..0a63795 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeRunner.kt @@ -0,0 +1,41 @@ +package com.github.biomejs.intellijbiome + +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.openapi.editor.Document +import com.intellij.openapi.util.NlsSafe +import com.intellij.openapi.vfs.VirtualFile +import org.jetbrains.annotations.Nls +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +interface BiomeRunner { + companion object { + val DEFAULT_TIMEOUT = 30000.milliseconds + } + + fun format(request: Request): Response + fun applySafeFixes(request: Request): Response + fun applyUnsafeFixes(request: Request): Response + fun createCommandLine(file: VirtualFile, action: String, args: String? = null): GeneralCommandLine + + + data class Request( + val document: Document, + val virtualFile: VirtualFile, + val timeout: Duration, + val commandDescription: String + ) + + sealed class Response { + class Success(val code: String) : Response() + + class Failure( + @Nls val title: String, + @NlsSafe val description: String, + val exitCode: Int? + ) : Response() + + } +} + + diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeStdinRunner.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeStdinRunner.kt new file mode 100644 index 0000000..bcf7098 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeStdinRunner.kt @@ -0,0 +1,122 @@ +package com.github.biomejs.intellijbiome + +import com.github.biomejs.intellijbiome.extensions.isSuccess +import com.github.biomejs.intellijbiome.extensions.runProcessFuture +import com.github.biomejs.intellijbiome.extensions.runWithNodeInterpreter +import com.intellij.execution.ExecutionException +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.openapi.progress.util.ProgressIndicatorUtils +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.SmartList +import java.io.File +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +class BiomeStdinRunner(private val project: Project) : BiomeRunner { + override fun format(request: BiomeRunner.Request): BiomeRunner.Response { + val commandLine = createCommandLine(request.virtualFile, "format") + val file = request.virtualFile + val timeout = request.timeout + val future = startTheFuture( + BiomeBundle.message( + "biome.failed.to.format.file", + file.name + ), timeout + ) + + commandLine.runProcessFuture().thenAccept { result -> + if (result.processEvent.isSuccess) { + future.complete(BiomeRunner.Response.Success(result.processOutput.stdout)) + } else { + future.complete( + BiomeRunner.Response.Failure( + BiomeBundle.message("biome.failed.to.format.file", file.name), + result.processOutput.stderr, result.processEvent.exitCode + ) + ) + } + } + + return ProgressIndicatorUtils.awaitWithCheckCanceled(future) + } + + override fun applySafeFixes(request: BiomeRunner.Request): BiomeRunner.Response { + val commandLine = createCommandLine(request.virtualFile, "lint", "--apply") + return runFixCommand(request, commandLine) + } + + override fun applyUnsafeFixes(request: BiomeRunner.Request): BiomeRunner.Response { + val commandLine = createCommandLine(request.virtualFile, "lint", "--apply-unsafe") + return runFixCommand(request, commandLine) + } + + private fun runFixCommand( + request: BiomeRunner.Request, + commandLine: GeneralCommandLine + ): BiomeRunner.Response { + val file = request.virtualFile + val timeout = request.timeout + val future = startTheFuture( + BiomeBundle.message( + "biome.failed.to.fix.file", + file.name + ), timeout + ) + + commandLine.runProcessFuture().thenAccept { result -> + if (result.processEvent.isSuccess) { + future.complete(BiomeRunner.Response.Success(result.processOutput.stdout)) + } else { + future.complete( + BiomeRunner.Response.Failure( + BiomeBundle.message("biome.failed.to.fix.file", file.name), + result.processOutput.stderr, result.processEvent.exitCode + ) + ) + } + } + + return ProgressIndicatorUtils.awaitWithCheckCanceled(future) + } + + override fun createCommandLine(file: VirtualFile, action: String, args: String?): GeneralCommandLine { + val configPath = BiomePackage.configPath(project) + val exePath = BiomePackage.binaryPath(project) + val params = SmartList(action, "--stdin-file-path", file.path) + + if (!args.isNullOrEmpty()) { + params.add(args) + } + + if (!configPath.isNullOrEmpty()) { + params.add("--config-path") + params.add(configPath) + } + + if (exePath.isNullOrEmpty()) { + throw ExecutionException(BiomeBundle.message("biome.language.server.not.found")) + } + + return GeneralCommandLine().runWithNodeInterpreter(project, exePath).apply { + withInput(File(file.path)) + addParameters(params) + } + } + + private fun startTheFuture( + timeoutMessage: String, + timeout: Duration + ): CompletableFuture { + val future = CompletableFuture() + .completeOnTimeout( + BiomeRunner.Response.Failure( + timeoutMessage, "Timeout exceeded", null + ), + timeout.inWholeMilliseconds, TimeUnit.MILLISECONDS + ) + return future + } + +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeUtils.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeUtils.kt deleted file mode 100644 index d8a154d..0000000 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/BiomeUtils.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.github.biomejs.intellijbiome - -import com.github.biomejs.intellijbiome.settings.BiomeSettings -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.util.ExecUtil -import com.intellij.javascript.nodejs.library.node_modules.NodeModulesDirectoryManager -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.project.Project -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.util.SmartList -import java.io.File -import com.intellij.javascript.nodejs.interpreter.NodeCommandLineConfigurator -import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager -import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreter -import com.intellij.javascript.nodejs.interpreter.wsl.WslNodeInterpreter -import com.intellij.execution.ExecutionException - -object BiomeUtils { - fun isSupportedFileType(file: VirtualFile): Boolean = when (file.extension) { - "js", "mjs", "cjs", "jsx", "ts", "mts", "cts", "tsx", "d.ts", "json", "jsonc" -> true - else -> false - } - - fun getBiomeVersion(project: Project, binaryPath: String): String? { - if (binaryPath.isEmpty()) { - return null - } - - val versionRegex = Regex("\\d{1,2}\\.\\d{1,2}\\.\\d{1,3}") - - val commandLine = createNodeCommandLine(project, binaryPath).apply { - addParameter("--version") - } - - val output = ExecUtil.execAndGetOutput(commandLine) - - val matchResult = versionRegex.find(output.stdout) - - return matchResult?.value - } - - fun getBiomeExecutablePath(project: Project): String? { - val directoryManager = NodeModulesDirectoryManager.getInstance(project) - val executablePath = BiomeSettings.getInstance(project).executablePath - - if (!executablePath.isEmpty()) { - return executablePath - } - - val biomeBinFile = directoryManager.nodeModulesDirs - .asSequence() - .mapNotNull { it.findFileByRelativePath("@biomejs/biome/bin/biome") } - .filter { it.isValid } - .firstOrNull() - - return biomeBinFile?.path - } - - fun getBiomeConfigPath(project: Project): String? { - val configPath = BiomeSettings.getInstance(project).configPath - - if (!configPath.isEmpty()) { - return configPath - } - - return null - } - - fun createNodeCommandLine(project: Project, executable: String): GeneralCommandLine { - val interpreter = NodeJsInterpreterManager.getInstance(project).interpreter - if (interpreter !is NodeJsLocalInterpreter && interpreter !is WslNodeInterpreter) { - throw ExecutionException(BiomeBundle.message("biome.interpreter.not.configured")) - } - - return GeneralCommandLine().apply { - withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) - addParameter(executable) - - NodeCommandLineConfigurator.find(interpreter) - .configure(this, NodeCommandLineConfigurator.defaultOptions(project)) - } - } -} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ApplySafeFixesOnSaveAction.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ApplySafeFixesOnSaveAction.kt new file mode 100644 index 0000000..0c283d3 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ApplySafeFixesOnSaveAction.kt @@ -0,0 +1,24 @@ +package com.github.biomejs.intellijbiome.actions + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.github.biomejs.intellijbiome.BiomeStdinRunner +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 + + +class ApplySafeFixesOnSaveAction : ActionsOnSaveFileDocumentManagerListener.ActionOnSave() { + override fun isEnabledForProject(project: Project): Boolean = + BiomeSettings.getInstance(project).applySafeFixesOnSave + + override fun processDocuments(project: Project, documents: Array) { + val runner = BiomeStdinRunner(project) + + OnSaveHelper().formatDocuments( + project, + documents.filterNotNull().toList(), + BiomeBundle.message("biome.apply.safe.fix.with.biome") + ) { request -> runner.applySafeFixes(request) } + } +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ApplyUnsafeFixesOnSaveAction.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ApplyUnsafeFixesOnSaveAction.kt new file mode 100644 index 0000000..1e09bb7 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ApplyUnsafeFixesOnSaveAction.kt @@ -0,0 +1,25 @@ +package com.github.biomejs.intellijbiome.actions + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.github.biomejs.intellijbiome.BiomeStdinRunner +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 + + +class ApplyUnsafeFixesOnSaveAction : ActionsOnSaveFileDocumentManagerListener.ActionOnSave() { + override fun isEnabledForProject(project: Project): Boolean = + BiomeSettings.getInstance(project).applyUnsafeFixesOnSave + + override fun processDocuments(project: Project, documents: Array) { + val runner = BiomeStdinRunner(project) + + OnSaveHelper().formatDocuments( + project, + documents.filterNotNull().toList(), + BiomeBundle.message("biome.apply.unsafe.fix.with.biome") + ) { request -> runner.applyUnsafeFixes(request) } + } + +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/FormatOnSaveAction.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/FormatOnSaveAction.kt new file mode 100644 index 0000000..7e4e925 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/FormatOnSaveAction.kt @@ -0,0 +1,23 @@ +package com.github.biomejs.intellijbiome.actions + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.github.biomejs.intellijbiome.BiomeStdinRunner +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 + + +class FormatOnSaveAction : ActionsOnSaveFileDocumentManagerListener.ActionOnSave() { + override fun isEnabledForProject(project: Project): Boolean = BiomeSettings.getInstance(project).formatOnSave + + override fun processDocuments(project: Project, documents: Array) { + val runner = BiomeStdinRunner(project) + + OnSaveHelper().formatDocuments( + project, + documents.filterNotNull().toList(), + BiomeBundle.message("biome.apply.safe.fix.with.biome") + ) { request -> runner.format(request) } + } +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/OnSaveHelper.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/OnSaveHelper.kt new file mode 100644 index 0000000..060a9ea --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/OnSaveHelper.kt @@ -0,0 +1,79 @@ +package com.github.biomejs.intellijbiome.actions + +import com.github.biomejs.intellijbiome.BiomeRunner +import com.github.biomejs.intellijbiome.settings.BiomeSettings +import com.intellij.lang.javascript.linter.GlobPatternUtil +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.progress.ProcessCanceledException +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project + +class OnSaveHelper { + companion object { + val LOG = thisLogger() + } + + fun formatDocuments( + project: Project, + documents: List, + commandDescription: String, + callback: (request: BiomeRunner.Request) -> BiomeRunner.Response + ) { + val manager = FileDocumentManager.getInstance() + val settings = BiomeSettings.getInstance(project) + val requests = documents + .mapNotNull { document -> manager.getFile(document)?.let { document to it } } + .filter { GlobPatternUtil.isFileMatchingGlobPattern(project, settings.formatFilePattern, it.second) } + .map { BiomeRunner.Request(it.first, it.second, BiomeRunner.DEFAULT_TIMEOUT, commandDescription) } + + runCatching { + ProgressManager.getInstance().run( + object : Task.Backgroundable(project, commandDescription, true) { + override fun run(indicator: ProgressIndicator) { + indicator.text = commandDescription + + requests.forEach { request -> + val response = callback(request) + + if (!indicator.isCanceled) { + applyChanges(project, request, response) + } + } + } + } + ) + }.onFailure { exception -> + when (exception) { + is ProcessCanceledException -> {} + + else -> { + LOG.error(exception) + } + } + } + } + + private fun applyChanges( + project: Project, + request: BiomeRunner.Request, + response: BiomeRunner.Response + ) { + when (response) { + is BiomeRunner.Response.Success -> { + WriteCommandAction.writeCommandAction(project) + .withName(request.commandDescription) + .run { request.document.setText(response.code) } + } + + is BiomeRunner.Response.Failure -> { + LOG.error(response.title) + } + } + } + +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ReformatWithBiomeAction.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ReformatWithBiomeAction.kt new file mode 100644 index 0000000..2324362 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/ReformatWithBiomeAction.kt @@ -0,0 +1,38 @@ +package com.github.biomejs.intellijbiome.actions + +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.project.DumbAware + + +class ReformatWithBiomeAction : AnAction(), DumbAware { + override fun actionPerformed(actionEvent: AnActionEvent) { + val project = actionEvent.project + if (project == null || project.isDefault) return + + val editor: Editor? = actionEvent.getData(CommonDataKeys.EDITOR) + + if (editor != null) { + FormatOnSaveAction().processDocuments(project, arrayOf(editor.document)) + } + } + + override fun update(actionEvent: AnActionEvent) { + val project = actionEvent.project + if (project == null || project.isDefault) { + actionEvent.presentation.isEnabledAndVisible = false + return + } + + val settings = BiomeSettings.getInstance(project) + actionEvent.presentation.isEnabledAndVisible = settings.isEnabled() + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/RestartBiomeServerAction.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/RestartBiomeServerAction.kt index aeab4f5..3fe7ceb 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/actions/RestartBiomeServerAction.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/actions/RestartBiomeServerAction.kt @@ -1,9 +1,9 @@ package com.github.biomejs.intellijbiome.actions +import com.github.biomejs.intellijbiome.services.BiomeServerService import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.components.service -import com.github.biomejs.intellijbiome.services.BiomeServerService class RestartBiomeServerAction : AnAction() { override fun actionPerformed(actionEvent: AnActionEvent) { diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/extensions/CommandLineExt.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/extensions/CommandLineExt.kt new file mode 100644 index 0000000..98a4a43 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/extensions/CommandLineExt.kt @@ -0,0 +1,52 @@ +package com.github.biomejs.intellijbiome.extensions + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.intellij.execution.ExecutionException +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.process.CapturingProcessAdapter +import com.intellij.execution.process.CapturingProcessHandler +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessOutput +import com.intellij.javascript.nodejs.interpreter.NodeCommandLineConfigurator +import com.intellij.javascript.nodejs.interpreter.NodeJsInterpreterManager +import com.intellij.javascript.nodejs.interpreter.local.NodeJsLocalInterpreter +import com.intellij.javascript.nodejs.interpreter.wsl.WslNodeInterpreter +import com.intellij.openapi.project.Project +import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletableFuture + +class ProcessResult(val processEvent: ProcessEvent, val processOutput: ProcessOutput) + +val ProcessEvent.isSuccess: Boolean get() = exitCode == 0 + +fun GeneralCommandLine.runProcessFuture(): CompletableFuture { + val future = CompletableFuture() + + val processHandler = CapturingProcessHandler(this.withCharset(StandardCharsets.UTF_8)) + + processHandler.addProcessListener(object : CapturingProcessAdapter() { + override fun processTerminated(event: ProcessEvent) { + future.complete(ProcessResult(event, output)) + } + }) + + processHandler.startNotify() + + return future +} + +fun GeneralCommandLine.runWithNodeInterpreter(project: Project, command: String): GeneralCommandLine { + val interpreter = NodeJsInterpreterManager.getInstance(project).interpreter + if (interpreter !is NodeJsLocalInterpreter && interpreter !is WslNodeInterpreter) { + throw ExecutionException(BiomeBundle.message("biome.interpreter.not.configured")) + } + + return this.apply { + withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) + addParameter(command) + withWorkDirectory(project.basePath) + + NodeCommandLineConfigurator.find(interpreter) + .configure(this, NodeCommandLineConfigurator.defaultOptions(project)) + } +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/formatter/BiomeFormatterProvider.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/formatter/FormatterProvider.kt similarity index 58% rename from src/main/kotlin/com/github/biomejs/intellijbiome/formatter/BiomeFormatterProvider.kt rename to src/main/kotlin/com/github/biomejs/intellijbiome/formatter/FormatterProvider.kt index 9089d6c..1756383 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/formatter/BiomeFormatterProvider.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/formatter/FormatterProvider.kt @@ -1,58 +1,42 @@ package com.github.biomejs.intellijbiome.formatter import com.github.biomejs.intellijbiome.BiomeBundle -import com.github.biomejs.intellijbiome.BiomeUtils -import com.intellij.execution.configurations.GeneralCommandLine +import com.github.biomejs.intellijbiome.BiomeStdinRunner +import com.github.biomejs.intellijbiome.settings.BiomeSettings +import com.intellij.execution.ExecutionException import com.intellij.execution.process.CapturingProcessAdapter import com.intellij.execution.process.OSProcessHandler import com.intellij.execution.process.ProcessEvent import com.intellij.formatting.service.AsyncDocumentFormattingService import com.intellij.formatting.service.AsyncFormattingRequest import com.intellij.formatting.service.FormattingService.Feature -import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.application.ApplicationInfo import com.intellij.psi.PsiFile -import com.intellij.util.SmartList import org.jetbrains.annotations.NotNull import java.nio.charset.StandardCharsets -import java.util.EnumSet -import com.intellij.execution.ExecutionException -import com.intellij.execution.process.CapturingProcessHandler -import com.intellij.openapi.progress.util.ProgressIndicatorBase - -class BiomeFormatterProvider : AsyncDocumentFormattingService() { - override fun getFeatures(): MutableSet = EnumSet.noneOf(Feature::class.java) - - override fun canFormat(file: PsiFile): Boolean = - file.virtualFile?.let { BiomeUtils.isSupportedFileType(it) } ?: false +import java.util.* - override fun getNotificationGroupId(): String = "Biome" - - override fun getName(): String = "Biome" +class FormatterProvider : AsyncDocumentFormattingService() { + override fun getFeatures(): MutableSet = FEATURES + override fun getNotificationGroupId(): String = NOTIFICATION_GROUP_ID + override fun getName(): String = NAME + override fun canFormat(file: PsiFile): Boolean { + // IDEs with version >= 2023.3 uses native LSP formatter + return ApplicationInfo.getInstance().build.baselineVersion < 233 + } override fun createFormattingTask(request: AsyncFormattingRequest): FormattingTask? { - val ioFile = request.ioFile ?: return null + val file = request.context.virtualFile ?: return null val project = request.context.project - val configPath = BiomeUtils.getBiomeConfigPath(project) - - val params = SmartList("format", "--stdin-file-path", ioFile.path) + val formatterRunner = BiomeStdinRunner(project) + val settings = BiomeSettings.getInstance(project) - if (!configPath.isNullOrEmpty()) { - params.add("--config-path") - params.add(configPath) - } - - val exePath = BiomeUtils.getBiomeExecutablePath(project) - - if (exePath.isNullOrEmpty()) { - throw ExecutionException(BiomeBundle.message("biome.language.server.not.found")) + if (!settings.canFormat(project, file)) { + return null } try { - val commandLine: GeneralCommandLine = BiomeUtils.createNodeCommandLine(project, exePath).apply { - withInput(ioFile) - addParameters(params) - withWorkDirectory(project.basePath) - } + val commandLine = formatterRunner.createCommandLine(file, "format") val handler = OSProcessHandler(commandLine.withCharset(StandardCharsets.UTF_8)) return object : FormattingTask { @@ -85,4 +69,10 @@ class BiomeFormatterProvider : AsyncDocumentFormattingService() { return null } } + + companion object { + val NAME: String = BiomeBundle.message("biome.formatting.service.name") + const val NOTIFICATION_GROUP_ID = "Biome" + val FEATURES: EnumSet = EnumSet.noneOf(Feature::class.java) + } } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/listeners/BiomeConfigListener.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/listeners/BiomeConfigListener.kt index c3d7980..c962cd2 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/listeners/BiomeConfigListener.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/listeners/BiomeConfigListener.kt @@ -1,10 +1,10 @@ package com.github.biomejs.intellijbiome.listeners +import com.github.biomejs.intellijbiome.services.BiomeServerService +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.newvfs.BulkFileListener import com.intellij.openapi.vfs.newvfs.events.VFileEvent -import com.intellij.openapi.components.service -import com.github.biomejs.intellijbiome.services.BiomeServerService class BiomeConfigListener(val project: Project) : BulkFileListener { override fun after(events: MutableList) { 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 345439e..2788a3f 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/lsp/BiomeLspServerSupportProvider.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/lsp/BiomeLspServerSupportProvider.kt @@ -1,63 +1,76 @@ package com.github.biomejs.intellijbiome.lsp import com.github.biomejs.intellijbiome.BiomeBundle -import com.github.biomejs.intellijbiome.BiomeUtils +import com.github.biomejs.intellijbiome.BiomePackage +import com.github.biomejs.intellijbiome.extensions.runWithNodeInterpreter import com.github.biomejs.intellijbiome.listeners.BIOME_CONFIG_RESOLVED_TOPIC +import com.github.biomejs.intellijbiome.settings.BiomeSettings import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.diagnostic.thisLogger 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.customization.LspCodeActionsSupport -import com.intellij.platform.lsp.api.customization.LspDiagnosticsSupport +import com.intellij.platform.lsp.api.customization.LspFormattingSupport import com.intellij.util.SmartList -import org.eclipse.lsp4j.* @Suppress("UnstableApiUsage") class BiomeLspServerSupportProvider : LspServerSupportProvider { - override fun fileOpened( project: Project, file: VirtualFile, serverStarter: LspServerSupportProvider.LspServerStarter ) { - if (BiomeUtils.isSupportedFileType(file)) { - val executable = BiomeUtils.getBiomeExecutablePath(project) ?: return - serverStarter.ensureServerStarted(BiomeLspServerDescriptor(project, executable)) - } + val executable = BiomePackage.binaryPath(project) ?: return + serverStarter.ensureServerStarted(LspServerDescriptor(project, executable)) } } @Suppress("UnstableApiUsage") -private class BiomeLspServerDescriptor(project: Project, val executable: String) : +private class LspServerDescriptor(project: Project, val executable: String) : ProjectWideLspServerDescriptor(project, "Biome") { - override fun isSupportedFile(file: VirtualFile) = BiomeUtils.isSupportedFileType(file) + + override fun isSupportedFile(file: VirtualFile): Boolean { + val settings = BiomeSettings.getInstance(project) + if (!settings.isEnabled()) { + return false + } + + return BiomeSettings.getInstance(project).canLint(project, file) + } + override fun createCommandLine(): GeneralCommandLine { - val configPath = BiomeUtils.getBiomeConfigPath(project) - val params = SmartList("lsp-proxy") + val configPath = BiomePackage.configPath(project) + val params = SmartList("lsp-proxy") - if (!configPath.isNullOrEmpty()) { - params.add("--config-path") - params.add(configPath) - } + if (!configPath.isNullOrEmpty()) { + params.add("--config-path") + params.add(configPath) + } if (executable.isEmpty()) { throw ExecutionException(BiomeBundle.message("biome.language.server.not.found")) } - val version = BiomeUtils.getBiomeVersion(project, executable) + val version = BiomePackage.versionNumber(project, executable) version?.let { project.messageBus.syncPublisher(BIOME_CONFIG_RESOLVED_TOPIC).resolved(it) } - return BiomeUtils.createNodeCommandLine(project, executable).apply { + return GeneralCommandLine().runWithNodeInterpreter(project, executable).apply { addParameters(params) } - } override val lspGoToDefinitionSupport = false override val lspCompletionSupport = null + + override val lspFormattingSupport = object : LspFormattingSupport() { + override fun shouldFormatThisFileExclusivelyByServer( + file: VirtualFile, + ideCanFormatThisFileItself: Boolean, + serverExplicitlyWantsToFormatThisFile: Boolean + ): Boolean { + return BiomeSettings.getInstance(project).canFormat(project, file) + } + } } 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 9a2ea95..47b6596 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/services/BiomeServerService.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/services/BiomeServerService.kt @@ -1,20 +1,23 @@ package com.github.biomejs.intellijbiome.services -import com.intellij.openapi.components.Service -import com.intellij.openapi.project.Project import com.github.biomejs.intellijbiome.BiomeBundle 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.project.Project import com.intellij.platform.lsp.api.LspServerManager @Service(Service.Level.PROJECT) class BiomeServerService(private val project: Project) { - fun restartBiomeServer() { LspServerManager.getInstance(project).stopAndRestartIfNeeded(BiomeLspServerSupportProvider::class.java) } + fun stopBiomeServer() { + LspServerManager.getInstance(project).stopServers(BiomeLspServerSupportProvider::class.java) + } + fun notifyRestart() { NotificationGroupManager.getInstance() .getNotificationGroup("Biome") diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeConfigurable.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeConfigurable.kt new file mode 100644 index 0000000..01844de --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeConfigurable.kt @@ -0,0 +1,257 @@ +package com.github.biomejs.intellijbiome.settings + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.github.biomejs.intellijbiome.services.BiomeServerService +import com.intellij.ide.actionsOnSave.ActionsOnSaveConfigurable +import com.intellij.lang.javascript.JavaScriptBundle +import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationNamesInfo +import com.intellij.openapi.components.service +import com.intellij.openapi.observable.properties.ObservableMutableProperty +import com.intellij.openapi.observable.util.whenItemSelected +import com.intellij.openapi.options.BoundSearchableConfigurable +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.ValidationInfo +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.ui.ContextHelpLabel +import com.intellij.ui.components.JBRadioButton +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.* +import com.intellij.ui.layout.ValidationInfoBuilder +import com.intellij.ui.layout.not +import com.intellij.ui.layout.selected +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.UIUtil +import java.nio.file.FileSystems +import java.util.regex.PatternSyntaxException +import javax.swing.JCheckBox +import javax.swing.JRadioButton +import javax.swing.text.JTextComponent + +private const val CONFIGURABLE_ID = "settings.biome" +private const val HELP_TOPIC = "reference.settings.biome" + +class BiomeConfigurable(internal val project: Project) : + BoundSearchableConfigurable( + BiomeBundle.message("biome.settings.name"), + HELP_TOPIC, + CONFIGURABLE_ID + ) { + lateinit var runFormatOnSaveCheckBox: JCheckBox + lateinit var runSafeFixesOnSaveCheckBox: JCheckBox + lateinit var runUnsafeFixesOnSaveCheckBox: JCheckBox + + private lateinit var disabledConfiguration: JRadioButton + private lateinit var automaticConfiguration: JRadioButton + private lateinit var manualConfiguration: JRadioButton + override fun createPanel(): DialogPanel { + val settings: BiomeSettings = BiomeSettings.getInstance(project) + val biomeServerService = project.service() + + return panel { + buttonsGroup { + row { + disabledConfiguration = + radioButton( + JavaScriptBundle.message( + "settings.javascript.linters.autodetect.disabled", + displayName + ) + ) + .bindSelected(ConfigurationModeProperty(settings, ConfigurationMode.DISABLED)) + .component + } + row { + automaticConfiguration = + radioButton( + JavaScriptBundle.message( + "settings.javascript.linters.autodetect.configure.automatically", + displayName + ) + ) + .bindSelected(ConfigurationModeProperty(settings, ConfigurationMode.AUTOMATIC)) + .component + + val detectAutomaticallyHelpText = JavaScriptBundle.message( + "settings.javascript.linters.autodetect.configure.automatically.help.text", + ApplicationNamesInfo.getInstance().fullProductName, + displayName, + "biome.json" + ) + + val helpLabel = ContextHelpLabel.create(detectAutomaticallyHelpText) + helpLabel.border = JBUI.Borders.emptyLeft(UIUtil.DEFAULT_HGAP) + cell(helpLabel) + } + row { + manualConfiguration = + radioButton( + JavaScriptBundle.message( + "settings.javascript.linters.autodetect.configure.manually", + displayName + ) + ) + .bindSelected(ConfigurationModeProperty(settings, ConfigurationMode.MANUAL)) + .component + } + } + panel { + row(BiomeBundle.message("biome.path.label")) { + textFieldWithBrowseButton(BiomeBundle.message("biome.path.label")) { fileChosen(it) } + .bindText(settings::executablePath) + }.visibleIf(manualConfiguration.selected) + + row(BiomeBundle.message("biome.config.path.label")) { + textFieldWithBrowseButton(BiomeBundle.message("biome.config.path.label")) { fileChosen(it) } + .bindText(settings::configPath) + }.visibleIf(manualConfiguration.selected) + } + + // ********************* + // Lint pattern row + // ********************* + row(BiomeBundle.message("biome.run.lint.for.files.label")) { + textField() + .comment(BiomeBundle.message("biome.files.pattern.comment")) + .align(AlignX.FILL) + .bind( + { textField -> textField.text.trim() }, + JTextComponent::setText, + MutableProperty({ settings.lintFilePattern }, { settings.lintFilePattern = it }) + ) + .validationOnInput(validateGlob()) + .component + }.enabledIf(!disabledConfiguration.selected) + + // ********************* + // Format pattern row + // ********************* + row(BiomeBundle.message("biome.run.format.for.files.label")) { + textField() + .align(AlignX.FILL) + .bind( + { textField -> textField.text.trim() }, + JTextComponent::setText, + MutableProperty({ settings.formatFilePattern }, { settings.formatFilePattern = it }) + ) + .validationOnInput(validateGlob()) + .component + }.enabledIf(!disabledConfiguration.selected) + + + // ********************* + // Format on save row + // ********************* + row { + runFormatOnSaveCheckBox = checkBox(BiomeBundle.message("biome.run.format.on.save.label")) + .bindSelected(RunOnObservableProperty( + { settings.configurationMode != ConfigurationMode.DISABLED && settings.formatOnSave }, + { settings.formatOnSave = it }, + { !disabledConfiguration.isSelected && runFormatOnSaveCheckBox.isSelected } + )) + .component + + val link = ActionsOnSaveConfigurable.createGoToActionsOnSavePageLink() + cell(link) + }.enabledIf(!disabledConfiguration.selected) + + // ********************* + // Apply safe fixes on save row + // ********************* + row { + runSafeFixesOnSaveCheckBox = checkBox(BiomeBundle.message("biome.run.safe.fixes.on.save.label")) + .bindSelected(RunOnObservableProperty( + { settings.configurationMode != ConfigurationMode.DISABLED && settings.applySafeFixesOnSave }, + { settings.applySafeFixesOnSave = it }, + { !disabledConfiguration.isSelected && runSafeFixesOnSaveCheckBox.isSelected } + )) + .component + + val link = ActionsOnSaveConfigurable.createGoToActionsOnSavePageLink() + cell(link) + }.enabledIf(!disabledConfiguration.selected) + + + // ********************* + // Apply unsafe fixes on save row + // ********************* + row { + runUnsafeFixesOnSaveCheckBox = checkBox(BiomeBundle.message("biome.run.unsafe.fixes.on.save.label")) + .bindSelected(RunOnObservableProperty( + { settings.configurationMode != ConfigurationMode.DISABLED && settings.applyUnsafeFixesOnSave }, + { settings.applyUnsafeFixesOnSave = it }, + { !disabledConfiguration.isSelected && runUnsafeFixesOnSaveCheckBox.isSelected } + )) + .component + + val link = ActionsOnSaveConfigurable.createGoToActionsOnSavePageLink() + cell(link) + }.enabledIf(!disabledConfiguration.selected) + + onApply { + biomeServerService.restartBiomeServer() + biomeServerService.notifyRestart() + } + } + + } + + private fun validateGlob(): ValidationInfoBuilder.(JBTextField) -> ValidationInfo? = + { + try { + FileSystems.getDefault().getPathMatcher("glob:" + it.text) + null + } catch (e: PatternSyntaxException) { + ValidationInfo(BiomeBundle.message("biome.invalid.pattern"), it) + } + } + + + private fun fileChosen(file: VirtualFile): String { + return file.path + } + + private class ConfigurationModeProperty( + private val settings: BiomeSettings, + private val mode: ConfigurationMode + ) : MutableProperty { + override fun get(): Boolean = + settings.configurationMode == mode + + override fun set(value: Boolean) { + if (value) { + settings.configurationMode = mode + } + } + } + + private inner class RunOnObservableProperty( + private val getter: () -> Boolean, + private val setter: (Boolean) -> Unit, + private val afterConfigModeChangeGetter: () -> Boolean, + ) : ObservableMutableProperty { + override fun set(value: Boolean) { + setter(value) + } + + override fun get(): Boolean = + getter() + + override fun afterChange(parentDisposable: Disposable?, listener: (Boolean) -> Unit) { + fun emitChange(radio: JBRadioButton) { + if (radio.isSelected) { + listener(afterConfigModeChangeGetter()) + } + } + + manualConfiguration.whenItemSelected(parentDisposable, ::emitChange) + automaticConfiguration.whenItemSelected(parentDisposable, ::emitChange) + disabledConfiguration.whenItemSelected(parentDisposable, ::emitChange) + } + } + + companion object { + const val CONFIGURABLE_ID = "Settings.Biome" + } +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveApplySafeFixesActionInfo.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveApplySafeFixesActionInfo.kt new file mode 100644 index 0000000..dca2403 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveApplySafeFixesActionInfo.kt @@ -0,0 +1,27 @@ +package com.github.biomejs.intellijbiome.settings + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.intellij.ide.actionsOnSave.ActionOnSaveBackedByOwnConfigurable +import com.intellij.ide.actionsOnSave.ActionOnSaveContext + +class BiomeOnSaveApplySafeFixesActionInfo(actionOnSaveContext: ActionOnSaveContext) : + ActionOnSaveBackedByOwnConfigurable( + actionOnSaveContext, + BiomeConfigurable.CONFIGURABLE_ID, + BiomeConfigurable::class.java + ) { + + override fun getActionOnSaveName() = + BiomeBundle.message("biome.run.safe.fixes.on.save.checkbox.on.actions.on.save.page") + + override fun isActionOnSaveEnabledAccordingToStoredState() = BiomeSettings.getInstance(project).applySafeFixesOnSave + + override fun isActionOnSaveEnabledAccordingToUiState(configurable: BiomeConfigurable) = + configurable.runSafeFixesOnSaveCheckBox.isSelected + + override fun setActionOnSaveEnabled(configurable: BiomeConfigurable, enabled: Boolean) { + configurable.runSafeFixesOnSaveCheckBox.isSelected = enabled + } + + override fun getActionLinks() = listOf(createGoToPageInSettingsLink(BiomeConfigurable.CONFIGURABLE_ID)) +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveApplyUnsafeFixesActionInfo.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveApplyUnsafeFixesActionInfo.kt new file mode 100644 index 0000000..180c7eb --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveApplyUnsafeFixesActionInfo.kt @@ -0,0 +1,28 @@ +package com.github.biomejs.intellijbiome.settings + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.intellij.ide.actionsOnSave.ActionOnSaveBackedByOwnConfigurable +import com.intellij.ide.actionsOnSave.ActionOnSaveContext + +class BiomeOnSaveApplyUnsafeFixesActionInfo(actionOnSaveContext: ActionOnSaveContext) : + ActionOnSaveBackedByOwnConfigurable( + actionOnSaveContext, + BiomeConfigurable.CONFIGURABLE_ID, + BiomeConfigurable::class.java + ) { + + override fun getActionOnSaveName() = + BiomeBundle.message("biome.run.unsafe.fixes.on.save.checkbox.on.actions.on.save.page") + + override fun isActionOnSaveEnabledAccordingToStoredState() = + BiomeSettings.getInstance(project).applyUnsafeFixesOnSave + + override fun isActionOnSaveEnabledAccordingToUiState(configurable: BiomeConfigurable) = + configurable.runUnsafeFixesOnSaveCheckBox.isSelected + + override fun setActionOnSaveEnabled(configurable: BiomeConfigurable, enabled: Boolean) { + configurable.runUnsafeFixesOnSaveCheckBox.isSelected = enabled + } + + override fun getActionLinks() = listOf(createGoToPageInSettingsLink(BiomeConfigurable.CONFIGURABLE_ID)) +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveFormatActionInfo.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveFormatActionInfo.kt new file mode 100644 index 0000000..7f7dad8 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveFormatActionInfo.kt @@ -0,0 +1,27 @@ +package com.github.biomejs.intellijbiome.settings + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.intellij.ide.actionsOnSave.ActionOnSaveBackedByOwnConfigurable +import com.intellij.ide.actionsOnSave.ActionOnSaveContext + +class BiomeOnSaveFormatActionInfo(actionOnSaveContext: ActionOnSaveContext) : + ActionOnSaveBackedByOwnConfigurable( + actionOnSaveContext, + BiomeConfigurable.CONFIGURABLE_ID, + BiomeConfigurable::class.java + ) { + + override fun getActionOnSaveName() = + BiomeBundle.message("biome.run.format.on.save.checkbox.on.actions.on.save.page") + + override fun isActionOnSaveEnabledAccordingToStoredState() = BiomeSettings.getInstance(project).formatOnSave + + override fun isActionOnSaveEnabledAccordingToUiState(configurable: BiomeConfigurable) = + configurable.runFormatOnSaveCheckBox.isSelected + + override fun setActionOnSaveEnabled(configurable: BiomeConfigurable, enabled: Boolean) { + configurable.runFormatOnSaveCheckBox.isSelected = enabled + } + + override fun getActionLinks() = listOf(createGoToPageInSettingsLink(BiomeConfigurable.CONFIGURABLE_ID)) +} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveInfoProvider.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveInfoProvider.kt new file mode 100644 index 0000000..a3a7045 --- /dev/null +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeOnSaveInfoProvider.kt @@ -0,0 +1,24 @@ +package com.github.biomejs.intellijbiome.settings + +import com.github.biomejs.intellijbiome.BiomeBundle +import com.intellij.ide.actionsOnSave.ActionOnSaveContext +import com.intellij.ide.actionsOnSave.ActionOnSaveInfo +import com.intellij.ide.actionsOnSave.ActionOnSaveInfoProvider + +class BiomeOnSaveInfoProvider : ActionOnSaveInfoProvider() { + override fun getActionOnSaveInfos(context: ActionOnSaveContext): + List = listOf( + BiomeOnSaveFormatActionInfo(context), + BiomeOnSaveApplySafeFixesActionInfo(context), + BiomeOnSaveApplyUnsafeFixesActionInfo(context) + ) + + override fun getSearchableOptions(): Collection { + return listOf( + BiomeBundle.message("biome.run.format.on.save.checkbox.on.actions.on.save.page"), + BiomeBundle.message("biome.run.safe.fixes.on.save.checkbox.on.actions.on.save.page"), + BiomeBundle.message("biome.run.unsafe.fixes.on.save.checkbox.on.actions.on.save.page") + ) + } +} + 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 e6d3db1..13ea298 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettings.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettings.kt @@ -1,7 +1,10 @@ package com.github.biomejs.intellijbiome.settings +import com.intellij.lang.javascript.linter.GlobPatternUtil import com.intellij.openapi.components.* import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile + @Service(Service.Level.PROJECT) @State(name = "BiomeSettings", storages = [(Storage("biome.xml"))]) @@ -18,6 +21,52 @@ class BiomeSettings : state.configPath = value } + var formatFilePattern: String + get() = state.formatFilePattern ?: BiomeSettingsState.DEFAULT_FILE_PATTERN + set(value) { + state.formatFilePattern = value + } + + var lintFilePattern: String + get() = state.lintFilePattern ?: BiomeSettingsState.DEFAULT_FILE_PATTERN + set(value) { + state.lintFilePattern = value + } + + var configurationMode: ConfigurationMode + get() = state.configurationMode + set(value) { + state.configurationMode = value + } + + var formatOnSave: Boolean + get() = isEnabled() && state.formatOnSave + set(value) { + state.formatOnSave = value + } + + var applySafeFixesOnSave: Boolean + get() = isEnabled() && state.applySafeFixesOnSave + set(value) { + state.applySafeFixesOnSave = value + } + + var applyUnsafeFixesOnSave: Boolean + get() = isEnabled() && state.applyUnsafeFixesOnSave + set(value) { + state.applyUnsafeFixesOnSave = value + } + + fun isEnabled(): Boolean { + return configurationMode !== ConfigurationMode.DISABLED + } + + fun canFormat(project: Project, file: VirtualFile): Boolean = + GlobPatternUtil.isFileMatchingGlobPattern(project, formatFilePattern, file) + + fun canLint(project: Project, file: VirtualFile): Boolean = + GlobPatternUtil.isFileMatchingGlobPattern(project, lintFilePattern, file) + companion object { @JvmStatic fun getInstance(project: Project): BiomeSettings = project.service() diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettingsConfigurable.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettingsConfigurable.kt deleted file mode 100644 index d24a146..0000000 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettingsConfigurable.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.biomejs.intellijbiome.settings - -import com.github.biomejs.intellijbiome.BiomeBundle -import com.github.biomejs.intellijbiome.services.BiomeServerService -import com.intellij.openapi.components.service -import com.intellij.openapi.options.BoundSearchableConfigurable -import com.intellij.openapi.project.Project -import com.intellij.openapi.ui.DialogPanel -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.panel - -class BiomeSettingsConfigurable(internal val project: Project) : - BoundSearchableConfigurable( - BiomeBundle.message("biome.settings.name"), - BiomeBundle.message("biome.settings.name") - ) { - override fun createPanel(): DialogPanel { - val settings: BiomeSettings = BiomeSettings.getInstance(project) - val biomeServerService = project.service() - - return panel { - row(BiomeBundle.message("biome.path.label")) { - textFieldWithBrowseButton(BiomeBundle.message("biome.path.label")) { fileChosen(it) } - .bindText(settings::executablePath) - } - - row(BiomeBundle.message("biome.config.path.label")) { - textFieldWithBrowseButton(BiomeBundle.message("biome.config.path.label")) { fileChosen(it) } - .bindText(settings::configPath) - } - - onApply { - biomeServerService.restartBiomeServer() - biomeServerService.notifyRestart() - } - } - - } - - fun fileChosen(file: VirtualFile): String { - return file.path - } -} diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettingsState.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettingsState.kt index ac2c6bc..48fda07 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettingsState.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/settings/BiomeSettingsState.kt @@ -4,9 +4,26 @@ import com.intellij.openapi.components.BaseState import com.intellij.openapi.components.Service import org.jetbrains.annotations.ApiStatus +@ApiStatus.Internal +enum class ConfigurationMode { + DISABLED, + AUTOMATIC, + MANUAL +} + @Service @ApiStatus.Internal class BiomeSettingsState : BaseState() { var executablePath by string() var configPath by string() + var formatFilePattern by string(DEFAULT_FILE_PATTERN) + var lintFilePattern by string(DEFAULT_FILE_PATTERN) + var formatOnSave by property(false) + var applySafeFixesOnSave by property(false) + var applyUnsafeFixesOnSave by property(false) + var configurationMode by enum(ConfigurationMode.AUTOMATIC) + + companion object { + const val DEFAULT_FILE_PATTERN = "**/*.{js,mjs,cjs,ts,jsx,tsx,cts,json,jsonc}" + } } diff --git a/src/main/kotlin/com/github/biomejs/intellijbiome/widgets/BiomeWidget.kt b/src/main/kotlin/com/github/biomejs/intellijbiome/widgets/BiomeWidget.kt index 0d674ff..c64a1a0 100644 --- a/src/main/kotlin/com/github/biomejs/intellijbiome/widgets/BiomeWidget.kt +++ b/src/main/kotlin/com/github/biomejs/intellijbiome/widgets/BiomeWidget.kt @@ -1,24 +1,19 @@ package com.github.biomejs.intellijbiome.widgets import com.github.biomejs.intellijbiome.BiomeBundle -import com.github.biomejs.intellijbiome.BiomeUtils +import com.github.biomejs.intellijbiome.BiomePackage import com.github.biomejs.intellijbiome.listeners.BIOME_CONFIG_RESOLVED_TOPIC import com.github.biomejs.intellijbiome.listeners.BiomeConfigResolvedListener import com.github.biomejs.intellijbiome.lsp.BiomeLspServerSupportProvider -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.ModalityState +import com.github.biomejs.intellijbiome.settings.BiomeSettings +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.progress.ProgressManager import com.intellij.openapi.project.Project -import com.intellij.openapi.wm.CustomStatusBarWidget +import com.intellij.openapi.wm.StatusBarWidget import com.intellij.openapi.wm.StatusBarWidget.WidgetPresentation -import com.intellij.openapi.wm.WindowManager import com.intellij.openapi.wm.impl.status.EditorBasedWidget -import com.intellij.openapi.wm.impl.status.TextPanel.WithIconAndArrows import com.intellij.platform.lsp.api.LspServerManager import com.intellij.platform.lsp.impl.LspServerImpl -import javax.swing.JComponent -import com.intellij.openapi.diagnostic.Logger -import com.intellij.openapi.progress.ProgressManager -import com.intellij.openapi.wm.StatusBarWidget class BiomeWidget(project: Project) : EditorBasedWidget(project), StatusBarWidget, StatusBarWidget.MultipleTextValuesPresentation { @@ -36,7 +31,7 @@ class BiomeWidget(project: Project) : EditorBasedWidget(project), StatusBarWidge } override fun ID(): String { - return javaClass.name; + return javaClass.name } override fun getPresentation(): WidgetPresentation { @@ -44,19 +39,22 @@ class BiomeWidget(project: Project) : EditorBasedWidget(project), StatusBarWidge } override fun getSelectedValue(): String? { - val biomeBin = BiomeUtils.getBiomeExecutablePath(project); - val progressManager = ProgressManager.getInstance() - - if (biomeBin == null) { - return "Biome" + val settings = BiomeSettings.getInstance(project) + if (!settings.isEnabled()) { + return null } + val binary = BiomePackage.binaryPath(project) + val progressManager = ProgressManager.getInstance() val version = progressManager.runProcessWithProgressSynchronously({ - BiomeUtils.getBiomeVersion(project, biomeBin) + BiomePackage.versionNumber(project, binary) }, BiomeBundle.message("biome.loading"), true, project) + if (version.isNullOrEmpty()) { + return null + } - return "Biome ${version}" + return BiomeBundle.message("biome.widget.version", version) } override fun getTooltipText(): String { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 00ba892..80e3813 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,9 +1,9 @@ - com.github.biomejs.intellijbiome - Biome - biomejs - com.github.biomejs.intellijbiome + Biome + biomejs + Biome plugin for JetBrains IDEs.

Features

@@ -16,43 +16,62 @@ ]]>
- com.intellij.modules.platform - com.intellij.modules.ultimate - JavaScript + com.intellij.modules.platform + com.intellij.modules.ultimate + JavaScript - messages.BiomeBundle + messages.BiomeBundle - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg index 3296987..3347d37 100644 --- a/src/main/resources/META-INF/pluginIcon.svg +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -1,4 +1,5 @@ - + diff --git a/src/main/resources/icons/pluginIcon.svg b/src/main/resources/icons/pluginIcon.svg index bed6c6e..3347d37 100644 --- a/src/main/resources/icons/pluginIcon.svg +++ b/src/main/resources/icons/pluginIcon.svg @@ -1,3 +1,5 @@ - + diff --git a/src/main/resources/messages/BiomeBundle.properties b/src/main/resources/messages/BiomeBundle.properties index 4a63296..9072d93 100644 --- a/src/main/resources/messages/BiomeBundle.properties +++ b/src/main/resources/messages/BiomeBundle.properties @@ -1,7 +1,8 @@ name=Biome +biome.formatting.service.name=Biome +biome.settings.name=Biome biome.path.label=Biome CLI Path -biome.config.path.label=biome.json Directory Path -biome.settings.name=Biome Settings +biome.config.path.label=Directory path of biome.json biome.formatting.failure=Formatting error biome.language.server.not.found=Biome language server is not found, make sure you have @biomejs/biome installed. biome.loading=Biome is loading... @@ -9,3 +10,20 @@ biome.language.server.is.running=Biome server is running biome.language.server.is.stopped=Biome server is stopped biome.language.server.restarted=Biome server restarted biome.interpreter.not.configured=Your node interpreter not configured. +biome.invalid.pattern=Invalid pattern +biome.run.format.for.files.label=Run format for &files: +biome.run.lint.for.files.label=Run lint for &files: +biome.run.format.on.save.label=Run format on &save +biome.run.safe.fixes.on.save.label=Run safe fixes on &save +biome.run.unsafe.fixes.on.save.label=Run unsafe fixes on &save +biome.files.pattern.comment=Use a glob pattern, for example, **/*.{js,ts} +biome.run.format.on.save.checkbox.on.actions.on.save.page=Run Biome format +biome.run.safe.fixes.on.save.checkbox.on.actions.on.save.page=Run Biome lint --apply +biome.run.unsafe.fixes.on.save.checkbox.on.actions.on.save.page=Run Biome lint --apply-unsafe +action.ReformatWithBiomeAction.text=Reformat with Biome +action.ReformatWithBiomeAction.description=Reformat with Biome +biome.apply.safe.fix.with.biome=Apply safe fixes with Biome +biome.apply.unsafe.fix.with.biome=Apply unsafe fixes with Biome +biome.failed.to.format.file=Biome failed to format the file +biome.failed.to.fix.file=Biome failed to fix the file +biome.widget.version=Biome {0} diff --git a/src/test/kotlin/com/github/biomejs/intellijbiome/BasicProjectNpmTest.kt b/src/test/kotlin/com/github/biomejs/intellijbiome/BasicProjectNpmTest.kt index ed63fc2..f3c1cc9 100644 --- a/src/test/kotlin/com/github/biomejs/intellijbiome/BasicProjectNpmTest.kt +++ b/src/test/kotlin/com/github/biomejs/intellijbiome/BasicProjectNpmTest.kt @@ -4,27 +4,25 @@ import com.github.biomejs.intellijbiome.pages.* import com.github.biomejs.intellijbiome.utils.RemoteRobotExtension import com.github.biomejs.intellijbiome.utils.StepsLogger import com.intellij.remoterobot.RemoteRobot -import com.intellij.remoterobot.fixtures.ComponentFixture import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.keyboard import com.intellij.remoterobot.utils.waitFor import com.intellij.remoterobot.utils.waitForIgnoringError -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import java.awt.Point import java.awt.event.KeyEvent.* import java.io.File -import java.time.Duration import java.time.Duration.ofMinutes @ExtendWith(RemoteRobotExtension::class) class BasicProjectNpmTest { - private val basicProjectPath = File("src/test/testData/basic-project") + init { StepsLogger.init() @@ -35,51 +33,30 @@ class BasicProjectNpmTest { waitForIgnoringError(ofMinutes(3)) { remoteRobot.callJs("true") } } - @AfterEach - fun closeProject(remoteRobot: RemoteRobot) = with(remoteRobot) { - idea { - if (remoteRobot.isMac()) { - keyboard { - hotKey(VK_SHIFT, VK_META, VK_A) - enterText("Close Project", 20) - enter() - } - } else { - menuBar.select("File", "Close Project") - } - } - } - @Test fun openQuickFixes(remoteRobot: RemoteRobot) = with(remoteRobot) { - welcomeFrame { - openProjectLink.click() - dialog("Open File or Project") { - directoryPath.text = basicProjectPath.absolutePath - button("OK").click() - } - } - idea { step("Check biome running version") { waitFor(ofMinutes(5)) { isDumbMode().not() } - openFile("index.js") - - val editor = editor("index.js") - - editor.click(Point(0, 0)) - - keyboard { - hotKey(VK_ALT, VK_ENTER) + step("Open file") { + openFile("index.js") + val editor = editor("index.js") + editor.click(Point(0, 0)) } - quickfix { - val items = collectItems() + step("Open quickfixes dialog") { + keyboard { + hotKey(VK_ALT, VK_ENTER) + } + quickfix { + val items = collectItems() + - assertTrue(items.contains("Use 'const' instead.")) - assertTrue(items.contains("Suppress rule lint/style/noVar")) + assertTrue(items.contains("Use 'const' instead.")) + assertTrue(items.contains("Suppress rule lint/style/noVar")) + } } keyboard { @@ -89,4 +66,28 @@ class BasicProjectNpmTest { } } + + companion object { + private val basicProjectPath = File("src/test/testData/basic-project") + + @JvmStatic + @BeforeAll + fun selectProject(remoteRobot: RemoteRobot) = with(remoteRobot) { + welcomeFrame { + openProjectLink.click() + dialog("Open File or Project") { + directoryPath.text = basicProjectPath.absolutePath + button("OK").click() + } + } + } + + @JvmStatic + @AfterAll + fun closeProject(remoteRobot: RemoteRobot) = with(remoteRobot) { + idea { + menuBar.select("File", "Close Project") + } + } + } } diff --git a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/DialogFixture.kt b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/DialogFixture.kt index 9f99668..3e709de 100644 --- a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/DialogFixture.kt +++ b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/DialogFixture.kt @@ -10,22 +10,24 @@ import com.intellij.remoterobot.stepsProcessing.step import java.time.Duration fun ContainerFixture.dialog( - title: String, - timeout: Duration = Duration.ofSeconds(20), - function: DialogFixture.() -> Unit = {}): DialogFixture = step("Search for dialog with title $title") { - find(DialogFixture.byTitle(title), timeout).apply(function) + title: String, + timeout: Duration = Duration.ofSeconds(20), + function: DialogFixture.() -> Unit = {} +): DialogFixture = step("Search for dialog with title $title") { + find(DialogFixture.byTitle(title), timeout).apply(function) } @FixtureName("Dialog") class DialogFixture( - remoteRobot: RemoteRobot, - remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) { + remoteRobot: RemoteRobot, + remoteComponent: RemoteComponent +) : CommonContainerFixture(remoteRobot, remoteComponent) { - companion object { - @JvmStatic - fun byTitle(title: String) = byXpath("title $title", "//div[@title='$title' and @class='MyDialog']") - } + companion object { + @JvmStatic + fun byTitle(title: String) = byXpath("title $title", "//div[@title='$title' and @class='MyDialog']") + } - val title: String - get() = callJs("component.getTitle();") + val title: String + get() = callJs("component.getTitle();") } diff --git a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/Editor.kt b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/Editor.kt index cdcc278..b109a8b 100644 --- a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/Editor.kt +++ b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/Editor.kt @@ -7,7 +7,6 @@ import com.intellij.remoterobot.fixtures.ComponentFixture import com.intellij.remoterobot.fixtures.ContainerFixture import com.intellij.remoterobot.fixtures.FixtureName import com.intellij.remoterobot.search.locators.byXpath -import java.awt.Point @JvmOverloads fun ContainerFixture.editor(title: String, function: Editor.() -> Unit = {}): Editor { diff --git a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/IdeaFrame.kt b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/IdeaFrame.kt index e22c8f4..e5245a1 100644 --- a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/IdeaFrame.kt +++ b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/IdeaFrame.kt @@ -2,42 +2,45 @@ package com.github.biomejs.intellijbiome.pages import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.data.RemoteComponent -import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName +import com.intellij.remoterobot.fixtures.JMenuBarFixture import com.intellij.remoterobot.stepsProcessing.step import com.intellij.remoterobot.utils.waitFor import java.time.Duration fun RemoteRobot.idea(function: IdeaFrame.() -> Unit) { - find(timeout = Duration.ofSeconds(10)).apply(function) + find(timeout = Duration.ofSeconds(10)).apply(function) } @FixtureName("Idea frame") @DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']") class IdeaFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : - CommonContainerFixture(remoteRobot, remoteComponent) { - val menuBar: JMenuBarFixture - get() = step("Menu...") { - return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType()) - } + CommonContainerFixture(remoteRobot, remoteComponent) { + val menuBar: JMenuBarFixture + get() = step("Menu...") { + return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType()) + } - @JvmOverloads - fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) { - step("Wait for smart mode") { - waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { - runCatching { isDumbMode().not() }.getOrDefault(false) - } - function() - step("..wait for smart mode again") { - waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { - isDumbMode().not() - } - } - } - } + @JvmOverloads + fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) { + step("Wait for smart mode") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + runCatching { isDumbMode().not() }.getOrDefault(false) + } + function() + step("..wait for smart mode again") { + waitFor(duration = timeout, interval = Duration.ofSeconds(5)) { + isDumbMode().not() + } + } + } + } - fun isDumbMode(): Boolean { - return callJs( - """ + fun isDumbMode(): Boolean { + return callJs( + """ const frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component) if (frameHelper) { const project = frameHelper.getProject() @@ -46,12 +49,12 @@ class IdeaFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : true } """, true - ) - } + ) + } - fun openFile(path: String) { - runJs( - """ + fun openFile(path: String) { + runJs( + """ importPackage(com.intellij.openapi.fileEditor) importPackage(com.intellij.openapi.vfs) importPackage(com.intellij.openapi.wm.impl) @@ -70,6 +73,6 @@ class IdeaFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : ) } """, true - ) - } + ) + } } diff --git a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/StatusBar.kt b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/StatusBar.kt index f6921e8..a25806c 100644 --- a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/StatusBar.kt +++ b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/StatusBar.kt @@ -2,22 +2,33 @@ package com.github.biomejs.intellijbiome.pages import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.data.RemoteComponent -import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.ContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName import com.intellij.remoterobot.search.locators.byXpath -import com.intellij.remoterobot.utils.waitFor import java.time.Duration + fun RemoteRobot.statusBar(function: StatusbarFrame.() -> Unit) { - find(timeout = Duration.ofSeconds(10)).apply(function) + find(timeout = Duration.ofSeconds(10)).apply(function) } @FixtureName("Statusbar frame") @DefaultXpath("IdeStatusBarImpl type", "//div[@class='IdeStatusBarImpl']") class StatusbarFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : - CommonContainerFixture(remoteRobot, remoteComponent) { + CommonContainerFixture(remoteRobot, remoteComponent) { + + val statusBarPanel + get() = find( + byXpath( + "StatusBarPanel", + "//div[@class='StatusBarPanel'][.//div[@class='CodeStyleStatusBarPanel']]" + ) + ) - val statusBarPanel get() = find(byXpath("StatusBarPanel", "//div[@class='StatusBarPanel'][.//div[@class='CodeStyleStatusBarPanel']]")) - fun byContainsText(text: String) = byXpath("text $text", "//div[contains(@text,'$text') and @class='WithIconAndArrows']") + fun byContainsText(text: String) = + byXpath("text $text", "//div[contains(@text,'$text') and @class='WithIconAndArrows']") - val text: String - get() = callJs("component.getText();") + val text: String + get() = callJs("component.getText();") } diff --git a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/WelcomeFrame.kt b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/WelcomeFrame.kt index e353c07..0d55130 100644 --- a/src/test/kotlin/com/github/biomejs/intellijbiome/pages/WelcomeFrame.kt +++ b/src/test/kotlin/com/github/biomejs/intellijbiome/pages/WelcomeFrame.kt @@ -2,7 +2,9 @@ package com.github.biomejs.intellijbiome.pages import com.intellij.remoterobot.RemoteRobot import com.intellij.remoterobot.data.RemoteComponent -import com.intellij.remoterobot.fixtures.* +import com.intellij.remoterobot.fixtures.CommonContainerFixture +import com.intellij.remoterobot.fixtures.DefaultXpath +import com.intellij.remoterobot.fixtures.FixtureName import com.intellij.remoterobot.search.locators.byXpath import java.time.Duration diff --git a/src/test/kotlin/com/github/biomejs/intellijbiome/utils/RemoteRobotExtension.kt b/src/test/kotlin/com/github/biomejs/intellijbiome/utils/RemoteRobotExtension.kt index 6cead4e..e9beec0 100644 --- a/src/test/kotlin/com/github/biomejs/intellijbiome/utils/RemoteRobotExtension.kt +++ b/src/test/kotlin/com/github/biomejs/intellijbiome/utils/RemoteRobotExtension.kt @@ -13,7 +13,6 @@ import org.junit.jupiter.api.extension.ParameterResolver import java.awt.image.BufferedImage import java.io.ByteArrayOutputStream import java.io.File -import java.lang.IllegalStateException import java.lang.reflect.Method import javax.imageio.ImageIO diff --git a/src/test/testData/basic-project/package.json b/src/test/testData/basic-project/package.json index 6b8a5c5..d704829 100644 --- a/src/test/testData/basic-project/package.json +++ b/src/test/testData/basic-project/package.json @@ -1,14 +1,14 @@ { "name": "basic-project", "version": "1.0.0", - "private": true, + "private": true, "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "license": "MIT OR Apache-2.0", - "dependencies": { - "@biomejs/biome": "workspace:*" - } + "dependencies": { + "@biomejs/biome": "workspace:*" + } }