diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f7f70c2..8eff82b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,7 @@ jobs: changelog: ${{ steps.properties.outputs.changelog }} pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} steps: + # Free GitHub Actions Environment Disk Space - name: Maximize Build Space uses: jlumbroso/free-disk-space@main @@ -108,6 +109,13 @@ jobs: runs-on: ubuntu-latest steps: + # Free GitHub Actions Environment Disk Space + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + large-packages: false + # Check out the current repository - name: Fetch Sources uses: actions/checkout@v4 @@ -178,6 +186,46 @@ jobs: with: cache-default-branch-only: true + detekt: + permission: + contents: read + security-events: write + runs-on: ubuntu-latest + steps: + # Free GitHub Actions Environment Disk Space + - name: Maximize Build Space + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + large-packages: false + + # Check out the current repository + - name: Fetch Sources + uses: actions/checkout@v4 + + # Set up Java environment for the next steps + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 17 + + # Setup Gradle + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + with: + gradle-home-cache-cleanup: true + + - name: Run Detekt + run: ./gradlew detektMain detektTest detektReportMergeSarif --continue + + - name: Upload Detekt Report + uses: github/codeql-action/upload-sarif@v3 + if: success() || failure() + with: + sarif_file: 'build/reports/detekt/merge.sarif.json' + + # Run plugin structure verification along with IntelliJ Plugin Verifier verify: name: Verify plugin @@ -227,40 +275,3 @@ jobs: with: name: pluginVerifier-result path: ${{ github.workspace }}/build/reports/pluginVerifier - - # Prepare a draft release for GitHub Releases page for the manual verification - # If accepted and published, release workflow would be triggered - releaseDraft: - name: Release draft - if: github.event_name != 'pull_request' - needs: [ build, test, inspectCode, verify ] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - # Check out the current repository - - name: Fetch Sources - uses: actions/checkout@v4 - - # Remove old release drafts by using the curl request for the available releases with a draft flag - - name: Remove Old Release Drafts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh api repos/{owner}/{repo}/releases \ - --jq '.[] | select(.draft == true) | .id' \ - | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} - - # Create a new release draft which is not publicly visible and requires manual acceptance - - name: Create Release Draft - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - gh release create "v${{ needs.build.outputs.version }}" \ - --draft \ - --title "v${{ needs.build.outputs.version }}" \ - --notes "$(cat << 'EOM' - ${{ needs.build.outputs.changelog }} - EOM - )" diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 32397a7e..f6dd5c95 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -14,7 +14,6 @@ diff --git a/CHANGELOG.md b/CHANGELOG.md index b1308347..a1ebca88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,57 @@ ## [Unreleased] +## [3.3.0] - 2025-01-16 + +### Added + +- Run Mise task as a run configuration. +- Unit tests for Mise code completion. + +### Fixed + +- Code completion is not working on `depends` single string. +- Mise task with quoted string cannot escape the quotes. + +## [3.2.6] - 2025-01-01 + +### Fixed + +- Jackson Deserialization failing on Unknown properties + +## [3.2.5] - 2024-12-28 + +### Fixed + +- Mise executable customization is not working +- Address new version of `mise tasks --json` output + +## [3.2.4] - 2024-12-28 + +### Fixed + +- Error report submitter cannot report an error when the error is too long. +- Mise Tools name is not showing correctly. +- Escape long-running tasks running on EDT. + +### Added + +- Add a mise cli version to the error report. + +## [3.2.3] - 2024-12-26 + +### Fixed + +- Fix environment variables are not loaded onto Gradle Task when triggered on Gradle Tool Window. + +## [3.2.2] - 2024-12-25 + +### Fixed + +- Fix runConfiguration cannot be saved +- Fix runConfiguration cannot load envvars when it doesn't have working directory. + - When the working directory is not set, it will use the project's base directory. + ## [3.2.1] - 2024-12-16 ### Added @@ -151,7 +202,13 @@ - Support JDK integration from mise tools. -[Unreleased]: https://github.com/134130/intellij-mise/compare/v3.2.1...HEAD +[Unreleased]: https://github.com/134130/intellij-mise/compare/v3.3.0...HEAD +[3.3.0]: https://github.com/134130/intellij-mise/compare/v3.2.6...v3.3.0 +[3.2.6]: https://github.com/134130/intellij-mise/compare/v3.2.5...v3.2.6 +[3.2.5]: https://github.com/134130/intellij-mise/compare/v3.2.4...v3.2.5 +[3.2.4]: https://github.com/134130/intellij-mise/compare/v3.2.3...v3.2.4 +[3.2.3]: https://github.com/134130/intellij-mise/compare/v3.2.2...v3.2.3 +[3.2.2]: https://github.com/134130/intellij-mise/compare/v3.2.1...v3.2.2 [3.2.1]: https://github.com/134130/intellij-mise/compare/v3.2.0...v3.2.1 [3.2.0]: https://github.com/134130/intellij-mise/compare/v3.1.0...v3.2.0 [3.1.0]: https://github.com/134130/intellij-mise/compare/v3.0.0...v3.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61a53a95..8a813792 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,30 @@ # Contributing to the IntelliJ Mise Plugin +Thank you for your interest in contributing to the IntelliJ Mise Plugin! + ## Running other IDEs for testing + As this plugin targets multiple IDEs, when developing you'll want to test against a specific IDE. You can get a list of available tasks for running the plugin in different IDEs by running the following command: + ```shell ./gradlew tasks --group='intellij platform' | grep -E '^run' ``` +> [!NOTE] +> In many cases, Intellij Ultimates supports other products via plugins. Maybe you just need `runIntellijIdeaUltimate` + +After that, you can run the desired IDE by running the following command: + +```shell +./gradlew prepareSandbox_runIntellijIdeaUltimate # only for the first time +./gradlew runIntellijIdeaUltimate +``` + +> [!NOTE] +> If you want to use breakpoints for debugging, you can just run the gradle task with IDEA's debug option + + > [!TIP] > If you want to test other version of IDEs, you can change the version field in the [build.gradle.kts](./build.gradle.kts) file. diff --git a/README.md b/README.md index 96392d5a..cd291d95 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,10 @@ from Mise config files. see: **[mise-en-place](https://mise.jdx.dev)** Download the [latest release](https://github.com/134130/intellij-mise/releases/latest) and install it manually using Settings/Preferences > Plugins > ⚙️ > Install plugin from disk... +## Ecosystem + +- See [mise-vscode](https://github.com/hverlin/mise-vscode/) if you are looking for a similar plugin for VSCode +- [Mise documentation](https://mise.jdx.dev/) --- Plugin based on the [IntelliJ Platform Plugin Template][template]. diff --git a/build.gradle.kts b/build.gradle.kts index c2c641c7..d65f76b8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,6 @@ +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.DetektCreateBaselineTask +import io.gitlab.arturbosch.detekt.report.ReportMergeTask import org.jetbrains.changelog.Changelog import org.jetbrains.changelog.markdownToHTML import org.jetbrains.intellij.platform.gradle.Constants.Constraints @@ -13,6 +16,7 @@ plugins { alias(libs.plugins.changelog) // Gradle Changelog Plugin alias(libs.plugins.qodana) // Gradle Qodana Plugin alias(libs.plugins.kover) // Gradle Kover Plugin + alias(libs.plugins.detekt) // Gradle Detekt Plugin } group = providers.gradleProperty("pluginGroup").get() @@ -35,11 +39,9 @@ dependencies { pluginModule(implementation(project(":mise-products-idea"))) pluginModule(implementation(project(":mise-products-nodejs"))) pluginModule(implementation(project(":mise-products-rider"))) - pluginModule(implementation(project(":mise-products-toml"))) plugins(listOf()) - instrumentationTools() pluginVerifier() zipSigner() testFramework(TestFrameworkType.Platform) @@ -139,6 +141,48 @@ tasks { } } +val detektReportMergeSarif by tasks.registering(ReportMergeTask::class) { + output = layout.buildDirectory.file("reports/detekt/merge.sarif.json") +} + +allprojects { + apply(plugin = "io.gitlab.arturbosch.detekt") + + detekt { + buildUponDefaultConfig = true + baseline = file("$rootDir/config/detekt/baseline.xml") + } + + dependencies { + detekt("io.gitlab.arturbosch.detekt:detekt-cli:${rootProject.libs.versions.detekt.get()}") + detekt("io.gitlab.arturbosch.detekt:detekt-formatting:${rootProject.libs.versions.detekt.get()}") + detekt(project(":mise-core")) + detekt(project(":mise-products-goland")) + detekt(project(":mise-products-gradle")) + detekt(project(":mise-products-idea")) + detekt(project(":mise-products-nodejs")) + detekt(project(":mise-products-rider")) + } + + tasks.withType().configureEach { + enabled = false + jvmTarget = "17" + reports { + xml.required = true + html.required = true + sarif.required = true + md.required = true + } + basePath = rootDir.absolutePath + } + detektReportMergeSarif { + input.from(tasks.withType().map { it.reports.sarif.outputLocation }) + } + tasks.withType().configureEach { + jvmTarget = "17" + } +} + val runIdeForUnitTests by intellijPlatformTesting.runIde.registering { task { jvmArgumentProviders += diff --git a/gradle.properties b/gradle.properties index 24b8b573..cf813efb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.l34130.mise pluginName = Mise pluginRepositoryUrl = https://github.com/134130/intellij-mise # SemVer format -> https://semver.org -pluginVersion=3.2.1 +pluginVersion=3.3.0 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild=233 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cb959fc7..d89b60da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ gradleIdeaExt = "1.1.9" kotlin = "2.1.0" kover = "0.9.0" qodana = "2024.2.6" +detekt = "1.23.7" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -18,3 +19,4 @@ gradleIdeaExt = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "gr kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } diff --git a/modules/core/build.gradle.kts b/modules/core/build.gradle.kts index 189bda0e..545e39e8 100644 --- a/modules/core/build.gradle.kts +++ b/modules/core/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import org.jetbrains.intellij.platform.gradle.TestFrameworkType fun properties(key: String) = project.findProperty(key).toString() @@ -8,16 +9,17 @@ plugins { } dependencies { - testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") + testImplementation(libs.junit) intellijPlatform { create(IntelliJPlatformType.IntellijIdeaCommunity, properties("platformVersion"), false) bundledPlugin("com.intellij.java") bundledPlugin("org.jetbrains.plugins.terminal") + bundledPlugin("org.toml.lang") jetbrainsRuntime() - instrumentationTools() + testFramework(TestFrameworkType.Platform) } } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseConstants.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseConstants.kt deleted file mode 100644 index a312e397..00000000 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseConstants.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.l34130.mise.core - -import com.intellij.openapi.externalSystem.model.ProjectSystemId - -object MiseConstants { - val SYSTEM_ID = ProjectSystemId("Mise") -} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseErrorReportSubmitter.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseErrorReportSubmitter.kt index e7583c44..a270b9b1 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseErrorReportSubmitter.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseErrorReportSubmitter.kt @@ -1,5 +1,6 @@ package com.github.l34130.mise.core +import com.github.l34130.mise.core.command.MiseCommandLine import com.github.l34130.mise.core.notification.MiseNotificationService import com.intellij.diagnostic.AbstractMessage import com.intellij.diagnostic.PluginException @@ -23,15 +24,13 @@ import java.net.URLEncoder import java.nio.charset.StandardCharsets class MiseErrorReportSubmitter : ErrorReportSubmitter() { - override fun getReportActionText(): String { - return "Report to Author" - } + override fun getReportActionText(): String = "Report to Author" override fun submit( events: Array, additionalInfo: String?, parentComponent: Component, - consumer: Consumer + consumer: Consumer, ): Boolean { val context = DataManager.getInstance().getDataContext(parentComponent) val project = CommonDataKeys.PROJECT.getData(context) @@ -59,43 +58,48 @@ class MiseErrorReportSubmitter : ErrorReportSubmitter() { StringUtils.abbreviate(throwable.message, 80) }" - val body = buildString { - appendLine("### Environment") - appendLine("- IDE: ${applicationNamesInfo.productName} ${appInfo.fullVersion}") - appendLine("- Plugin: ${pluginDescriptor.name} $${pluginDescriptor.releaseVersion}") - appendLine("- OS: ${System.getProperty("os.name")} ${System.getProperty("os.version")}") - appendLine("- Java: ${System.getProperty("java.version")} (${System.getProperty("java.vendor")})") + val body = + buildString { + appendLine("### Environment") + appendLine("- IDE: ${applicationNamesInfo.productName} ${appInfo.fullVersion}") + appendLine("- Plugin: ${pluginDescriptor.name} ${pluginDescriptor.version}") + appendLine("- Mise: mise@${MiseCommandLine.getMiseVersion()}") + appendLine("- OS: ${System.getProperty("os.name")} ${System.getProperty("os.version")}") + appendLine("- Java: ${System.getProperty("java.version")} (${System.getProperty("java.vendor")})") - appendLine() + appendLine() - appendLine("### Description") - appendLine("- $description") + appendLine("### Description") + appendLine("- $description") - appendLine() + appendLine() - appendLine("### Stacktrace") - appendLine("```") - appendLine(throwable.message) - appendLine(throwable.stackTrace.joinToString("\n")) - appendLine("```") + appendLine("### Stacktrace") + appendLine("```") + appendLine(throwable.message) + appendLine(throwable.stackTrace.joinToString("\n").take(25)) + appendLine("```") - appendLine() + appendLine() - appendLine("### Attachments") - for (attachment in attachments) { - appendLine("- $attachment") + appendLine("### Attachments") + for (attachment in attachments) { + appendLine("- $attachment") + } } - } - Desktop.getDesktop() + Desktop + .getDesktop() .browse( URI.create( buildString { - append("https://github.com/134130/intellij-mise/issues/new?labels=bug,${applicationNamesInfo.productName}") + append( + "https://github.com/134130/intellij-mise/issues/new?labels=bug,${applicationNamesInfo.productName}", + ) append("&title=${URLEncoder.encode(title, StandardCharsets.UTF_8)}") append("&body=${URLEncoder.encode(body, StandardCharsets.UTF_8)}") - } - ) + }, + ), ) } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseHelper.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseHelper.kt new file mode 100644 index 00000000..b4f46e23 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/MiseHelper.kt @@ -0,0 +1,45 @@ +package com.github.l34130.mise.core + +import com.github.l34130.mise.core.command.MiseCommandLineHelper +import com.github.l34130.mise.core.command.MiseCommandLineNotFoundException +import com.github.l34130.mise.core.notification.MiseNotificationServiceUtils +import com.github.l34130.mise.core.run.MiseRunConfigurationSettingsEditor +import com.github.l34130.mise.core.setting.MiseSettings +import com.intellij.execution.configurations.RunConfigurationBase +import com.intellij.openapi.components.service +import com.intellij.util.application +import java.util.function.Supplier + +object MiseHelper { + fun getMiseEnvVarsOrNotify( + configuration: RunConfigurationBase<*>, + workingDirectory: Supplier, + ): Map { + val project = configuration.project + val projectState = application.service().state + val runConfigState = MiseRunConfigurationSettingsEditor.getMiseRunConfigurationState(configuration) + + val (workDir, configEnvironment) = + when { + projectState.useMiseDirEnv -> { + project.basePath to projectState.miseConfigEnvironment + } + runConfigState?.useMiseDirEnv == true -> { + (workingDirectory.get() ?: project.basePath) to runConfigState.miseConfigEnvironment + } + else -> return emptyMap() + } + + return MiseCommandLineHelper + .getEnvVars(workDir, configEnvironment) + .fold( + onSuccess = { envVars -> envVars }, + onFailure = { + if (it !is MiseCommandLineNotFoundException) { + MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) + } + emptyMap() + }, + ) + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseRunTaskOnTerminalAction.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/action/MiseRunTaskOnTerminalAction.kt similarity index 88% rename from modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseRunTaskOnTerminalAction.kt rename to modules/core/src/main/kotlin/com/github/l34130/mise/core/action/MiseRunTaskOnTerminalAction.kt index ddf4764d..1ca30e1e 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseRunTaskOnTerminalAction.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/action/MiseRunTaskOnTerminalAction.kt @@ -1,7 +1,8 @@ -package com.github.l34130.mise.core.command +package com.github.l34130.mise.core.action import com.github.l34130.mise.core.util.TerminalUtils import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.DumbAwareAction @@ -15,6 +16,8 @@ class MiseRunTaskOnTerminalAction( "Execute the Mise task on Terminal", AllIcons.Actions.Execute, ) { + override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT + override fun actionPerformed(e: AnActionEvent) { val project = e.project if (project == null) { diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseCommandLine.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseCommandLine.kt index d1e98ab2..425cb89d 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseCommandLine.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseCommandLine.kt @@ -1,26 +1,37 @@ package com.github.l34130.mise.core.command -import com.google.gson.FieldNamingPolicy -import com.google.gson.GsonBuilder -import com.google.gson.reflect.TypeToken +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import com.github.l34130.mise.core.setting.MiseSettings import com.intellij.execution.ExecutionException import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.util.ExecUtil +import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger - -private val LOG = Logger.getInstance(MiseCommandLine::class.java) +import com.intellij.util.application +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread internal class MiseCommandLine( private val workDir: String? = null, private val configEnvironment: String? = null, ) { - inline fun runCommandLine(vararg params: String): Result = - runCommandLine(params.toList()) + inline fun runCommandLine(vararg params: String): Result = runCommandLine(params.toList()) inline fun runCommandLine(params: List): Result { + val typeReference = object : TypeReference() {} + return runCommandLine(params, typeReference) + } + + fun runCommandLine( + params: List, + typeReference: TypeReference, + ): Result { val miseVersion = getMiseVersion() - val commandLineArgs = mutableListOf("mise") + val commandLineArgs = mutableListOf(application.service().state.executablePath) if (configEnvironment != null) { if (miseVersion >= MiseVersion(2024, 12, 2)) { @@ -34,31 +45,35 @@ internal class MiseCommandLine( commandLineArgs.addAll(params) - return if (T::class == String::class) { - runCommandLine(commandLineArgs) { it as T } - } else { - runCommandLine(commandLineArgs) { - GsonBuilder() - .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES) - .create() - .fromJson(it, object : TypeToken() {}) - } + return runCommandLine(commandLineArgs) { + ObjectMapper() + .registerKotlinModule() + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE) + .readValue(it, typeReference) } } - private fun runCommandLine(commandLineArgs: List, transform: (String) -> T): Result { + private fun runCommandLine( + commandLineArgs: List, + transform: (String) -> T, + ): Result { val generalCommandLine = GeneralCommandLine(commandLineArgs).withWorkDirectory(workDir) - val processOutput = try { - ExecUtil.execAndGetOutput(generalCommandLine, 5000) - } catch (e: ExecutionException) { - return Result.failure( - MiseCommandLineNotFoundException( - generalCommandLine, - e.message ?: "Failed to execute command.", - e + val processOutput = + try { + logger.debug("Running command: $commandLineArgs") + ExecUtil.execAndGetOutput(generalCommandLine, 5000) + } catch (e: ExecutionException) { + logger.info("Failed to execute command. (command=$generalCommandLine)", e) + return Result.failure( + MiseCommandLineNotFoundException( + generalCommandLine, + e.message ?: "Failed to execute command.", + e, + ), ) - ) - } + } if (!processOutput.isExitCodeSet) { when { @@ -76,31 +91,38 @@ internal class MiseCommandLine( val stderr = processOutput.stderr val parsedError = MiseCommandLineException.parseFromStderr(generalCommandLine, stderr) if (parsedError == null) { - LOG.error("Failed to parse error. (exitCode=${processOutput.exitCode}, command=$generalCommandLine)\n$stderr") + logger.info("Failed to parse error from stderr. (stderr=$stderr)") return Result.failure(Throwable(stderr)) } else { + logger.debug("Parsed error from stderr. (error=$parsedError)") return Result.failure(parsedError) } } + logger.debug("Command executed successfully. (command=$generalCommandLine)") return Result.success(transform(processOutput.stdout)) } companion object { + @RequiresBackgroundThread fun getMiseVersion(): MiseVersion { val miseCommandLine = MiseCommandLine() - val versionString = miseCommandLine.runCommandLine(listOf("mise", "version")) { it } - - val miseVersion = versionString.fold( - onSuccess = { - MiseVersion.parse(it) - }, - onFailure = { _ -> - MiseVersion(0, 0, 0) - } - ) + val miseExecutable = application.service().state.executablePath + val versionString = miseCommandLine.runCommandLine(listOf(miseExecutable, "version")) { it } + + val miseVersion = + versionString.fold( + onSuccess = { + MiseVersion.parse(it) + }, + onFailure = { _ -> + MiseVersion(0, 0, 0) + }, + ) return miseVersion } + + private val logger = Logger.getInstance(MiseCommandLine::class.java) } } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseCommandLineHelper.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseCommandLineHelper.kt index 741a7d09..8c464974 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseCommandLineHelper.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseCommandLineHelper.kt @@ -1,8 +1,14 @@ package com.github.l34130.mise.core.command +import com.intellij.util.concurrency.annotations.RequiresBackgroundThread + object MiseCommandLineHelper { // mise env - fun getEnvVars(workDir: String?, configEnvironment: String?): Result> { + @RequiresBackgroundThread + fun getEnvVars( + workDir: String?, + configEnvironment: String?, + ): Result> { val commandLineArgs = mutableListOf("env", "--json") val miseCommandLine = MiseCommandLine(workDir, configEnvironment) @@ -10,7 +16,11 @@ object MiseCommandLineHelper { } // mise ls - fun getDevTools(workDir: String?, configEnvironment: String?): Result>> { + @RequiresBackgroundThread + fun getDevTools( + workDir: String?, + configEnvironment: String?, + ): Result>> { val commandLineArgs = mutableListOf("ls", "--current", "--json") val miseVersion = MiseCommandLine.getMiseVersion() @@ -22,14 +32,19 @@ object MiseCommandLineHelper { } val miseCommandLine = MiseCommandLine(workDir, configEnvironment) - return miseCommandLine.runCommandLine>>(commandLineArgs) + return miseCommandLine + .runCommandLine>>(commandLineArgs) .map { devTools -> devTools.mapKeys { (toolName, _) -> MiseDevToolName(toolName) } } } // mise task ls - fun getTasks(workDir: String?, configEnvironment: String?): Result> { + @RequiresBackgroundThread + fun getTasks( + workDir: String?, + configEnvironment: String?, + ): Result> { val commandLineArgs = mutableListOf("task", "ls", "--json") val miseCommandLine = MiseCommandLine(workDir, configEnvironment) @@ -37,6 +52,7 @@ object MiseCommandLineHelper { } // mise trust + @RequiresBackgroundThread fun trustConfigFile(configFilePath: String): Result { val commandLineArgs = mutableListOf("trust", configFilePath) diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseDevToolName.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseDevToolName.kt index c42e7a87..50e1362c 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseDevToolName.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseDevToolName.kt @@ -34,4 +34,6 @@ value class MiseDevToolName( "docker" to "Docker", ) } + + override fun toString(): String = canonicalName() } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseSource.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseSource.kt index 3624ee4e..d0130669 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseSource.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/command/MiseSource.kt @@ -1,6 +1,10 @@ package com.github.l34130.mise.core.command +import com.fasterxml.jackson.annotation.JsonProperty + data class MiseSource( + @JsonProperty("type") val fileName: String, // .mise.toml, .mise.config, etc. + @JsonProperty("path") val absolutePath: String, // absolute path to the source file ) diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/MiseTomlTaskRunLineMarkerContributor.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/MiseTomlTaskRunLineMarkerContributor.kt new file mode 100644 index 00000000..2091943a --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/MiseTomlTaskRunLineMarkerContributor.kt @@ -0,0 +1,27 @@ +package com.github.l34130.mise.core.execution + +import com.github.l34130.mise.core.lang.MiseTomlFileType +import com.github.l34130.mise.core.lang.psi.MiseTomlFile +import com.github.l34130.mise.core.lang.psi.MiseTomlPsiPatterns +import com.github.l34130.mise.core.lang.psi.isSpecificTaskTableHeader +import com.github.l34130.mise.core.lang.psi.miseTomlTask +import com.intellij.execution.lineMarker.RunLineMarkerContributor +import com.intellij.icons.AllIcons +import com.intellij.psi.PsiElement +import org.toml.lang.psi.TomlKey +import org.toml.lang.psi.TomlKeySegment +import org.toml.lang.psi.TomlTableHeader + +class MiseTomlTaskRunLineMarkerContributor : RunLineMarkerContributor() { + override fun getInfo(element: PsiElement): Info? { + if (element.containingFile.fileType !is MiseTomlFileType) return null + val tomlKey = element.parent.parent as? TomlKey ?: return null + val header = tomlKey.parent as? TomlTableHeader ?: return null + val miseTomlTask = if (header.isSpecificTaskTableHeader) { + header.miseTomlTask.takeIf { header.key?.segments?.getOrNull(1) == element.parent } + } else { + header.miseTomlTask + } ?: return null + return Info(AllIcons.Actions.Execute, { "" }, RunMiseTomlTaskAction(miseTomlTask)) + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/RunMiseTomlTaskAction.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/RunMiseTomlTaskAction.kt new file mode 100644 index 00000000..d648c64f --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/RunMiseTomlTaskAction.kt @@ -0,0 +1,40 @@ +package com.github.l34130.mise.core.execution + +import com.github.l34130.mise.core.execution.configuration.MiseTomlTaskRunConfigurationProducer +import com.intellij.execution.Executor +import com.intellij.execution.Location +import com.intellij.execution.PsiLocation +import com.intellij.execution.RunManagerEx +import com.intellij.execution.actions.ConfigurationContext +import com.intellij.execution.runners.ExecutionUtil +import com.intellij.icons.AllIcons +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.impl.SimpleDataContext +import org.toml.lang.psi.TomlKeySegment + +internal class RunMiseTomlTaskAction( + private val miseTomlTask: TomlKeySegment, +) : AnAction( + "Run Mise Toml Task", + "Execute the Mise Toml task", + AllIcons.Actions.Execute, + ) { + override fun actionPerformed(event: AnActionEvent) { + val dataContext = + SimpleDataContext.getSimpleContext( + Location.DATA_KEY, + PsiLocation(miseTomlTask), + event.dataContext, + ) + + val context = ConfigurationContext.getFromContext(dataContext, event.place) + + val producer = MiseTomlTaskRunConfigurationProducer() + val configuration = producer.findOrCreateConfigurationFromContext(context)?.configurationSettings ?: return + + val runManager = (context.runManager as? RunManagerEx) ?: return + runManager.setTemporaryConfiguration(configuration) + ExecutionUtil.runConfiguration(configuration, Executor.EXECUTOR_EXTENSION_NAME.extensionList.first()) + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfiguration.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfiguration.kt new file mode 100644 index 00000000..d653e97b --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfiguration.kt @@ -0,0 +1,99 @@ +package com.github.l34130.mise.core.execution.configuration + +import com.github.l34130.mise.core.setting.MiseSettings +import com.intellij.execution.Executor +import com.intellij.execution.configuration.EnvironmentVariablesData +import com.intellij.execution.configurations.CommandLineState +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.LocatableConfigurationBase +import com.intellij.execution.configurations.PtyCommandLine +import com.intellij.execution.configurations.RunConfiguration +import com.intellij.execution.configurations.RunProfileState +import com.intellij.execution.process.ColoredProcessHandler +import com.intellij.execution.process.ProcessHandler +import com.intellij.execution.process.ProcessTerminatedListener +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.openapi.components.PathMacroManager +import com.intellij.openapi.components.service +import com.intellij.openapi.options.SettingsEditor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.vfs.encoding.EncodingManager +import com.intellij.util.EnvironmentUtil +import com.intellij.util.application +import org.jdom.Element + +class MiseTomlTaskRunConfiguration( + project: Project, + factory: MiseTomlTaskRunConfigurationFactory, + name: String, +) : LocatableConfigurationBase(project, factory, name) { + private val settings = application.service().state + + var miseExecutablePath: String = settings.executablePath + var miseConfigEnvironment: String = settings.miseConfigEnvironment + var miseTaskName: String = "" + var workingDirectory: String? = project.basePath + var envVars: EnvironmentVariablesData = EnvironmentVariablesData.DEFAULT + + override fun getState( + executor: Executor, + executionEnvironment: ExecutionEnvironment, + ): RunProfileState { + return object : CommandLineState(executionEnvironment) { + override fun startProcess(): ProcessHandler { + val macroManager = PathMacroManager.getInstance(project) + val workDirectory = + workingDirectory?.let { macroManager.expandPath(it) } + ?: project.basePath + + val params = mutableListOf() + if (miseConfigEnvironment.isNotBlank()) { + params += listOf("--env", miseConfigEnvironment) + } + params += listOf("run", miseTaskName) + + val commandLine = PtyCommandLine() + if(!SystemInfo.isWindows) { + commandLine.withEnvironment("TERM", "xterm-256color") + } + commandLine.withConsoleMode(false) + commandLine.withInitialColumns(120) + commandLine.withCharset(EncodingManager.getInstance().defaultConsoleEncoding) + commandLine.withEnvironment(EnvironmentUtil.getEnvironmentMap() + envVars.envs) + commandLine.withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) + commandLine.withWorkDirectory(workDirectory) + + commandLine.withExePath(miseExecutablePath) + commandLine.withParameters(params) + + return ColoredProcessHandler(commandLine).apply { + setShouldKillProcessSoftly(true) + ProcessTerminatedListener.attach(this) + } + } + } + } + + override fun getConfigurationEditor(): SettingsEditor = MiseTomlTaskRunConfigurationEditor(project) + + override fun writeExternal(element: Element) { + super.writeExternal(element) + val child = element.getOrCreateChild("mise") + child.setAttribute("executablePath", miseExecutablePath) + child.setAttribute("configEnvironment", miseConfigEnvironment ?: "") + child.setAttribute("taskName", miseTaskName) + child.setAttribute("workingDirectory", workingDirectory ?: "") + envVars.writeExternal(child) + } + + override fun readExternal(element: Element) { + super.readExternal(element) + val child = element.getChild("mise") ?: return + miseExecutablePath = child.getAttributeValue("executablePath") ?: "" + miseConfigEnvironment = child.getAttributeValue("configEnvironment") + miseTaskName = child.getAttributeValue("taskName") ?: "" + workingDirectory = child.getAttributeValue("workingDirectory") + envVars = EnvironmentVariablesData.readExternal(child) + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationEditor.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationEditor.kt new file mode 100644 index 00000000..79942dab --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationEditor.kt @@ -0,0 +1,81 @@ +package com.github.l34130.mise.core.execution.configuration + +import com.github.l34130.mise.core.lang.MiseTomlFileType +import com.github.l34130.mise.core.setting.MiseSettings +import com.intellij.execution.configuration.EnvironmentVariablesComponent +import com.intellij.openapi.components.service +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory +import com.intellij.openapi.options.SettingsEditor +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.TextFieldWithBrowseButton +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.panel +import com.intellij.util.application +import com.intellij.util.ui.JBUI +import javax.swing.JComponent + +class MiseTomlTaskRunConfigurationEditor( + private val project: Project, +) : SettingsEditor() { + private val applicationState = application.service().state + + private val miseExecutablePathTf = TextFieldWithBrowseButton() + private val miseConfigEnvironmentTf = JBTextField() + private val miseTaskNameTf = JBTextField() + private val workingDirectoryTf = TextFieldWithBrowseButton() + private val envVarsComponent = EnvironmentVariablesComponent() + + init { + miseExecutablePathTf.addBrowseFolderListener( + "Mise Executable Path", + "Select the Mise executable path", + project, + FileChooserDescriptorFactory.createSingleFileDescriptor(MiseTomlFileType), + ) + workingDirectoryTf.addBrowseFolderListener( + "Working Directory", + "Select the working directory", + project, + FileChooserDescriptorFactory.createSingleFolderDescriptor(), + ) + } + + override fun createEditor(): JComponent = + panel { + row("Mise executable:") { + cell(miseExecutablePathTf).align(AlignX.FILL) + } + row("Mise config environment:") { + cell(miseConfigEnvironmentTf).align(AlignX.FILL) + } + row("Mise task name:") { + cell(miseTaskNameTf).align(AlignX.FILL) + } + row("Working directory:") { + cell(workingDirectoryTf).align(AlignX.FILL) + } + row(envVarsComponent.label) { + cell(envVarsComponent.component).align(AlignX.FILL) + } + }.apply { + border = JBUI.Borders.empty(6, 16, 16, 16) + } + + override fun resetEditorFrom(configuration: MiseTomlTaskRunConfiguration) { + miseExecutablePathTf.text = configuration.miseExecutablePath + miseConfigEnvironmentTf.text = configuration.miseConfigEnvironment + miseTaskNameTf.text = configuration.miseTaskName + workingDirectoryTf.text = configuration.workingDirectory ?: "" + envVarsComponent.envData = configuration.envVars + } + + override fun applyEditorTo(configuration: MiseTomlTaskRunConfiguration) { + configuration.miseExecutablePath = miseExecutablePathTf.text + configuration.miseConfigEnvironment = miseConfigEnvironmentTf.text + configuration.miseTaskName = miseTaskNameTf.text + configuration.workingDirectory = workingDirectoryTf.text + configuration.envVars = envVarsComponent.envData + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationFactory.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationFactory.kt new file mode 100644 index 00000000..ceaf0ae0 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationFactory.kt @@ -0,0 +1,20 @@ +package com.github.l34130.mise.core.execution.configuration + +import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.execution.configurations.RunConfiguration +import com.intellij.openapi.project.Project +import org.toml.lang.psi.TomlKeySegment + +class MiseTomlTaskRunConfigurationFactory( + private val runConfigurationType: MiseTomlTaskRunConfigurationType, +) : ConfigurationFactory(runConfigurationType) { + override fun getId(): String = "MiseTomlTask" + + override fun getName(): String = "Mise Toml Task" + + override fun createTemplateConfiguration(project: Project): RunConfiguration = MiseTomlTaskRunConfiguration(project, this, "name") + + fun createConfigurationFromMiseTomlTask(segment: TomlKeySegment): MiseTomlTaskRunConfiguration? { + TODO() + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationProducer.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationProducer.kt new file mode 100644 index 00000000..dbdb4758 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationProducer.kt @@ -0,0 +1,47 @@ +package com.github.l34130.mise.core.execution.configuration + +import com.github.l34130.mise.core.lang.psi.MiseTomlFile +import com.github.l34130.mise.core.lang.psi.taskName +import com.intellij.execution.actions.ConfigurationContext +import com.intellij.execution.actions.LazyRunConfigurationProducer +import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.openapi.components.PathMacroManager +import com.intellij.openapi.util.Ref +import com.intellij.psi.PsiElement +import com.intellij.psi.util.parentOfType +import org.toml.lang.psi.TomlTable + +class MiseTomlTaskRunConfigurationProducer : LazyRunConfigurationProducer() { + override fun getConfigurationFactory(): ConfigurationFactory = + MiseTomlTaskRunConfigurationFactory(MiseTomlTaskRunConfigurationType.getInstance()) + + override fun isConfigurationFromContext( + configuration: MiseTomlTaskRunConfiguration, + context: ConfigurationContext, + ): Boolean = configuration.miseTaskName == findTaskName(context) + + override fun setupConfigurationFromContext( + configuration: MiseTomlTaskRunConfiguration, + context: ConfigurationContext, + sourceElement: Ref, + ): Boolean { + if (context.psiLocation?.containingFile !is MiseTomlFile) return false + + val taskName = findTaskName(context) ?: return false + configuration.miseTaskName = taskName + + val macroManager = PathMacroManager.getInstance(context.project) + + val virtualFile = context.location?.virtualFile + configuration.workingDirectory = macroManager.collapsePath(virtualFile?.parent?.path ?: context.project.basePath) + + configuration.name = "Run $taskName" + + return true + } + + private fun findTaskName(context: ConfigurationContext): String? { + val table = context.psiLocation?.parentOfType() ?: return null + return table.taskName + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationType.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationType.kt new file mode 100644 index 00000000..5030b7e7 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/execution/configuration/MiseTomlTaskRunConfigurationType.kt @@ -0,0 +1,24 @@ +package com.github.l34130.mise.core.execution.configuration + +import com.github.l34130.mise.core.icon.MiseIcons +import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.execution.configurations.ConfigurationType +import com.intellij.execution.configurations.ConfigurationTypeUtil +import javax.swing.Icon + +class MiseTomlTaskRunConfigurationType : ConfigurationType { + override fun getDisplayName(): String = "Mise Toml Task" + + override fun getConfigurationTypeDescription(): String = "Mise Toml Task run configuration" + + override fun getIcon(): Icon = MiseIcons.DEFAULT + + override fun getId(): String = javaClass.simpleName + + override fun getConfigurationFactories(): Array = arrayOf(MiseTomlTaskRunConfigurationFactory(this)) + + companion object { + fun getInstance(): MiseTomlTaskRunConfigurationType = + ConfigurationTypeUtil.findConfigurationType(MiseTomlTaskRunConfigurationType::class.java) + } +} diff --git a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/MiseFileType.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/MiseTomlFileType.kt similarity index 79% rename from modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/MiseFileType.kt rename to modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/MiseTomlFileType.kt index 4f1c67d3..c6b75e7d 100644 --- a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/MiseFileType.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/MiseTomlFileType.kt @@ -1,11 +1,11 @@ -package com.github.l34130.mise.toml +package com.github.l34130.mise.core.lang import com.github.l34130.mise.core.icon.MiseIcons import com.intellij.openapi.fileTypes.LanguageFileType import org.toml.lang.TomlLanguage import javax.swing.Icon -object MiseFileType : LanguageFileType(TomlLanguage) { +object MiseTomlFileType : LanguageFileType(TomlLanguage) { override fun getName(): String = "mise" override fun getDescription(): String = "Mise Configuration file" diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/InsertionHandlers.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/InsertionHandlers.kt new file mode 100644 index 00000000..630a6412 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/InsertionHandlers.kt @@ -0,0 +1,37 @@ +package com.github.l34130.mise.core.lang.completion + +import com.intellij.codeInsight.completion.InsertHandler +import com.intellij.codeInsight.completion.InsertionContext +import com.intellij.codeInsight.lookup.LookupElement +import com.intellij.psi.PsiElement +import com.intellij.psi.tree.TokenSet +import com.intellij.psi.util.PsiTreeUtil +import org.toml.lang.psi.TomlElementTypes +import org.toml.lang.psi.TomlLiteral +import org.toml.lang.psi.ext.elementType + +class StringLiteralInsertionHandler : InsertHandler { + override fun handleInsert( + context: InsertionContext, + item: LookupElement, + ) { + val leaf = context.getElementOfType() ?: return + val elementType = (leaf.parent as? TomlLiteral)?.elementType + + val hasQuotes = elementType in tokenSet + if (!hasQuotes) { + context.document.insertString(context.startOffset, "\"") + context.document.insertString(context.selectionEndOffset, "\"") + } + } + + private val tokenSet = + TokenSet.create( + TomlElementTypes.BASIC_STRING, + TomlElementTypes.LITERAL_STRING, + TomlElementTypes.LITERAL, + ) +} + +inline fun InsertionContext.getElementOfType(strict: Boolean = false): T? = + PsiTreeUtil.findElementOfClassAtOffset(file, tailOffset - 1, T::class.java, strict) diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlCompletionContributor.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlCompletionContributor.kt new file mode 100644 index 00000000..1c6222b9 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlCompletionContributor.kt @@ -0,0 +1,16 @@ +package com.github.l34130.mise.core.lang.completion + +import com.github.l34130.mise.core.lang.psi.MiseTomlPsiPatterns +import com.github.l34130.mise.core.lang.psi.or +import com.intellij.codeInsight.completion.CompletionContributor +import com.intellij.codeInsight.completion.CompletionType + +class MiseTomlCompletionContributor : CompletionContributor() { + init { + extend( + CompletionType.BASIC, + MiseTomlPsiPatterns.inTaskDependsArray or MiseTomlPsiPatterns.inTaskDependsString, + MiseTomlTaskDependsCompletionProvider(), + ) + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlTaskDependsCompletionProvider.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlTaskDependsCompletionProvider.kt new file mode 100644 index 00000000..f736f7d7 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlTaskDependsCompletionProvider.kt @@ -0,0 +1,64 @@ +package com.github.l34130.mise.core.lang.completion + +import com.github.l34130.mise.core.lang.psi.MiseTomlFile +import com.github.l34130.mise.core.lang.psi.allTasks +import com.github.l34130.mise.core.lang.psi.stringValue +import com.github.l34130.mise.core.lang.psi.taskName +import com.intellij.codeInsight.completion.CompletionParameters +import com.intellij.codeInsight.completion.CompletionProvider +import com.intellij.codeInsight.completion.CompletionResultSet +import com.intellij.codeInsight.lookup.LookupElementBuilder +import com.intellij.psi.util.parentOfType +import com.intellij.util.ProcessingContext +import org.toml.lang.psi.TomlArray +import org.toml.lang.psi.TomlTable + +/** + * ``` + * [tasks.foo] + * run = "..." + * ``` + * + * then + * + * ``` + * [tasks.] + * depends = [ "f" ] + * #^ Provides completion for "foo" + * ``` + * + * or + * + * ``` + * [tasks.] + * depends = "f" + * #^ Provides completion for "foo" + * ``` + */ +class MiseTomlTaskDependsCompletionProvider : CompletionProvider() { + override fun addCompletions( + parameters: CompletionParameters, + context: ProcessingContext, + result: CompletionResultSet, + ) { + val element = parameters.position + val miseTomlFile = element.containingFile as? MiseTomlFile ?: return + + val dependsArray = (element.parent.parent as? TomlArray) + + val parentTable = element.parentOfType() ?: return + val parentTaskName = parentTable.taskName + + for (task in miseTomlFile.allTasks()) { + val taskName = task.name ?: continue + if (dependsArray?.elements?.any { it.stringValue == taskName } == true) continue + if (taskName == parentTaskName) continue + + result.addElement( + LookupElementBuilder + .createWithSmartPointer(taskName, task) + .withInsertHandler(StringLiteralInsertionHandler()), + ) + } + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/json/MiseTomlJsonSchemaFileProviderFactory.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/json/MiseTomlJsonSchemaFileProviderFactory.kt new file mode 100644 index 00000000..567c751b --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/json/MiseTomlJsonSchemaFileProviderFactory.kt @@ -0,0 +1,27 @@ +package com.github.l34130.mise.core.lang.json + +import com.github.l34130.mise.core.lang.MiseTomlFileType +import com.intellij.openapi.project.DumbAware +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider +import com.jetbrains.jsonSchema.extension.JsonSchemaProviderFactory +import com.jetbrains.jsonSchema.extension.SchemaType + +class MiseTomlJsonSchemaFileProviderFactory : + JsonSchemaProviderFactory, + DumbAware { + override fun getProviders(project: Project): List = listOf(MiseTomlJsonSchemaFileProvider()) + + class MiseTomlJsonSchemaFileProvider : JsonSchemaFileProvider { + override fun isAvailable(file: VirtualFile): Boolean = file.fileType is MiseTomlFileType + + override fun getName(): String = "mise" + + override fun getSchemaType(): SchemaType = SchemaType.embeddedSchema + + override fun isUserVisible(): Boolean = false + + override fun getSchemaFile(): VirtualFile? = JsonSchemaProviderFactory.getResourceFile(javaClass, "/schemas/mise.json") + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/json/MiseTomlPsiWalkerFactory.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/json/MiseTomlPsiWalkerFactory.kt new file mode 100644 index 00000000..762eadef --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/json/MiseTomlPsiWalkerFactory.kt @@ -0,0 +1,14 @@ +package com.github.l34130.mise.core.lang.json + +import com.github.l34130.mise.core.lang.MiseTomlFileType +import com.intellij.psi.PsiElement +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalker +import com.jetbrains.jsonSchema.extension.JsonLikePsiWalkerFactory +import com.jetbrains.jsonSchema.impl.JsonSchemaObject +import org.toml.ide.json.TomlJsonPsiWalker + +class MiseTomlPsiWalkerFactory : JsonLikePsiWalkerFactory { + override fun handles(element: PsiElement): Boolean = element.containingFile.fileType is MiseTomlFileType + + override fun create(schemaObject: JsonSchemaObject): JsonLikePsiWalker = TomlJsonPsiWalker +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlFile.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlFile.kt new file mode 100644 index 00000000..ee648edd --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlFile.kt @@ -0,0 +1,14 @@ +package com.github.l34130.mise.core.lang.psi + +import com.github.l34130.mise.core.lang.MiseTomlFileType +import com.intellij.extapi.psi.PsiFileBase +import com.intellij.psi.FileViewProvider +import org.toml.lang.TomlLanguage + +class MiseTomlFile( + viewProvider: FileViewProvider, +) : PsiFileBase(viewProvider, TomlLanguage) { + override fun getFileType() = MiseTomlFileType + + override fun toString() = "Mise Toml File" +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlParserDefinition.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlParserDefinition.kt new file mode 100644 index 00000000..bd69f983 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlParserDefinition.kt @@ -0,0 +1,36 @@ +package com.github.l34130.mise.core.lang.psi + +import com.intellij.lang.ASTNode +import com.intellij.lang.ParserDefinition +import com.intellij.lang.PsiParser +import com.intellij.lexer.Lexer +import com.intellij.openapi.project.Project +import com.intellij.psi.FileViewProvider +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.tree.IFileElementType +import com.intellij.psi.tree.TokenSet +import org.toml.lang.TomlLanguage +import org.toml.lang.lexer.TomlLexer +import org.toml.lang.parse.TomlParser +import org.toml.lang.psi.TOML_COMMENTS + +class MiseTomlParserDefinition : ParserDefinition { + override fun createLexer(project: Project): Lexer = TomlLexer() + + override fun createParser(project: Project): PsiParser = TomlParser() + + override fun getFileNodeType(): IFileElementType = FILE + + override fun getCommentTokens(): TokenSet = TOML_COMMENTS + + override fun getStringLiteralElements(): TokenSet = TokenSet.EMPTY + + override fun createElement(node: ASTNode): PsiElement = throw UnsupportedOperationException() + + override fun createFile(viewProvider: FileViewProvider): PsiFile = MiseTomlFile(viewProvider) + + companion object { + val FILE = IFileElementType(TomlLanguage) + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiPatterns.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiPatterns.kt new file mode 100644 index 00000000..2b697ff1 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiPatterns.kt @@ -0,0 +1,117 @@ +package com.github.l34130.mise.core.lang.psi + +import com.github.l34130.mise.core.lang.MiseTomlFileType +import com.intellij.patterns.ElementPattern +import com.intellij.patterns.ObjectPattern +import com.intellij.patterns.PatternCondition +import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.PsiElementPattern +import com.intellij.patterns.VirtualFilePattern +import com.intellij.psi.PsiElement +import com.intellij.util.ProcessingContext +import org.toml.lang.psi.TomlArray +import org.toml.lang.psi.TomlKeyValue +import org.toml.lang.psi.TomlLiteral +import org.toml.lang.psi.TomlTable +import org.toml.lang.psi.TomlTableHeader +import org.toml.lang.psi.ext.TomlLiteralKind +import org.toml.lang.psi.ext.kind +import org.toml.lang.psi.ext.name + +object MiseTomlPsiPatterns { + private inline fun miseTomlPsiElement(): PsiElementPattern.Capture = + psiElement().inVirtualFile( + VirtualFilePattern().ofType(MiseTomlFileType), + ) + + fun miseTomlStringLiteral() = miseTomlPsiElement().with("stringLiteral") { e, _ -> e.kind is TomlLiteralKind.String } + + private val onSpecificTaskTable = + miseTomlPsiElement() + .withChild( + psiElement() + .with("specificTaskCondition") { header, _ -> + header.isSpecificTaskTableHeader + }, + ) + + /** + * ``` + * [tasks] + * foo = { $name = [] } + * #^ + * ``` + * + * ``` + * [tasks.foo] + * $name = [] + * #^ + * ``` + */ + private fun taskProperty(name: String) = + psiElement() + .with("name") { e, _ -> e.key.name == name } + .withParent( + onSpecificTaskTable, +// onSpecificTaskTable.andOr( +// psiElement().withSuperParent(2, onTaskTable), +// ), + ) + + /** + * ``` + * [tasks] + * foo = { version = "*", depends = [] } + * #^ + * ``` + * + * ``` + * [tasks.foo] + * depends = [] + * #^ + * ``` + */ + val onTaskDependsArray = + psiElement().withParent(taskProperty("depends")) or + psiElement().withParent(taskProperty("depends_post")) + val inTaskDependsArray = miseTomlPsiElement().inside(onTaskDependsArray) + + /** + * ``` + * [tasks] + * foo = { version = "*", depends = "" } + * #^ + * ``` + * + * ``` + * [tasks.foo] + * depends = "" + * #^ + * ``` + */ + val onTaskDependsString = + miseTomlStringLiteral().withParent(taskProperty("depends")) or + miseTomlStringLiteral().withParent(taskProperty("depends_post")) + + val inTaskDependsString = miseTomlPsiElement().inside(onTaskDependsString) + + fun > ObjectPattern.with( + name: String, + cond: (T, ProcessingContext?) -> Boolean, + ): Self = + with( + object : PatternCondition(name) { + override fun accepts( + t: T, + context: ProcessingContext?, + ): Boolean = cond(t, context) + }, + ) + +} + +inline fun psiElement(): PsiElementPattern.Capture = PlatformPatterns.psiElement(I::class.java) + +inline infix fun ElementPattern.or(pattern: ElementPattern): PsiElementPattern.Capture { + return psiElement().andOr(this, pattern) +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiUtils.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiUtils.kt new file mode 100644 index 00000000..49ca213d --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiUtils.kt @@ -0,0 +1,72 @@ +package com.github.l34130.mise.core.lang.psi + +import com.intellij.psi.PsiElementResolveResult +import com.intellij.psi.ResolveResult +import com.intellij.psi.util.childrenOfType +import org.toml.lang.psi.TomlKeySegment +import org.toml.lang.psi.TomlKeyValueOwner +import org.toml.lang.psi.TomlLiteral +import org.toml.lang.psi.TomlTable +import org.toml.lang.psi.TomlTableHeader +import org.toml.lang.psi.TomlValue +import org.toml.lang.psi.ext.TomlLiteralKind +import org.toml.lang.psi.ext.kind + +fun MiseTomlFile.allTasks(): Sequence { + val explicitTasks = hashSetOf() + + return childrenOfType() + .asSequence() + .flatMap { table -> + val header = table.header + when { + // [tasks.] + header.isSpecificTaskTableHeader -> { + val lastKey = header.key?.segments?.last() + if (lastKey != null && lastKey.name !in explicitTasks) { + sequenceOf(lastKey) + } else { + emptySequence() + } + } + else -> emptySequence() + } + }.constrainOnce() +} + +fun MiseTomlFile.resolveTask(taskName: String): Sequence = + allTasks() + .filter { it.name == taskName } + .map { PsiElementResolveResult(it) } + +val TomlTable.taskName: String? + get() { + if (header.isSpecificTaskTableHeader) { + val headerKey = header.key ?: return null + return headerKey.segments.lastOrNull()?.name + } + return null + } + +val TomlTableHeader.miseTomlTask: TomlKeySegment? + get() { + if (isSpecificTaskTableHeader) { + val headerKey = key ?: return null + return headerKey.segments.lastOrNull() + } + return null + } + +val TomlTableHeader.isSpecificTaskTableHeader: Boolean + get() { + val names = key?.segments.orEmpty() + return names.getOrNull(names.size - 2)?.name == "tasks" + } + +fun TomlKeyValueOwner.getValueWithKey(key: String): TomlValue? = entries.find { it.key.text == key }?.value + +val TomlValue.stringValue: String? + get() { + val kind = (this as? TomlLiteral)?.kind + return (kind as? TomlLiteralKind.String)?.value + } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/resolve/MiseTomlReferenceContributor.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/resolve/MiseTomlReferenceContributor.kt new file mode 100644 index 00000000..06070744 --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/resolve/MiseTomlReferenceContributor.kt @@ -0,0 +1,18 @@ +package com.github.l34130.mise.core.lang.resolve + +import com.github.l34130.mise.core.lang.psi.MiseTomlPsiPatterns +import com.intellij.patterns.PlatformPatterns +import com.intellij.psi.PsiReferenceContributor +import com.intellij.psi.PsiReferenceRegistrar + +class MiseTomlReferenceContributor : PsiReferenceContributor() { + override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) { + registrar.registerReferenceProvider( + PlatformPatterns.or( + MiseTomlPsiPatterns.miseTomlStringLiteral().withParent(MiseTomlPsiPatterns.onTaskDependsArray), + MiseTomlPsiPatterns.onTaskDependsString, + ), + MiseTomlTaskDependsReferenceProvider(), + ) + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/resolve/MiseTomlTaskDependsReferenceProvider.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/resolve/MiseTomlTaskDependsReferenceProvider.kt new file mode 100644 index 00000000..26d6485e --- /dev/null +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/lang/resolve/MiseTomlTaskDependsReferenceProvider.kt @@ -0,0 +1,45 @@ +package com.github.l34130.mise.core.lang.resolve + +import com.github.l34130.mise.core.lang.psi.MiseTomlFile +import com.github.l34130.mise.core.lang.psi.resolveTask +import com.github.l34130.mise.core.lang.psi.stringValue +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiPolyVariantReferenceBase +import com.intellij.psi.PsiReference +import com.intellij.psi.PsiReferenceProvider +import com.intellij.psi.ResolveResult +import com.intellij.util.ProcessingContext +import org.toml.lang.psi.TomlLiteral + +/** + * ``` + * [tasks.foo] + * run = "echo foo" + * ``` + * + * ``` + * [tasks.bar] + * depends = [ "foo" ] + * #^ Provides a reference for "foo" + * ``` + */ +class MiseTomlTaskDependsReferenceProvider : PsiReferenceProvider() { + override fun getReferencesByElement( + element: PsiElement, + context: ProcessingContext, + ): Array { + if (element !is TomlLiteral) return PsiReference.EMPTY_ARRAY + return arrayOf(MiseTomlTaskDependsReference(element)) + } + + private class MiseTomlTaskDependsReference( + element: TomlLiteral, + ) : PsiPolyVariantReferenceBase(element) { + override fun multiResolve(incompleteCode: Boolean): Array { + val literalValue = element.stringValue ?: return ResolveResult.EMPTY_ARRAY + val miseTomlFile = element.containingFile as? MiseTomlFile ?: return ResolveResult.EMPTY_ARRAY + + return miseTomlFile.resolveTask(literalValue).toList().toTypedArray() + } + } +} diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/run/MiseRunConfigurationSettingsEditor.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/run/MiseRunConfigurationSettingsEditor.kt index 233d503a..79eef5cf 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/run/MiseRunConfigurationSettingsEditor.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/run/MiseRunConfigurationSettingsEditor.kt @@ -14,56 +14,58 @@ import com.intellij.ui.dsl.builder.columns import com.intellij.ui.dsl.builder.panel import com.intellij.ui.layout.ComponentPredicate import com.intellij.ui.layout.and +import com.intellij.ui.layout.not import com.intellij.ui.layout.selected +import com.intellij.util.application import org.jdom.Element -import java.awt.BorderLayout import javax.swing.JComponent -import javax.swing.JPanel private val USER_DATA_KEY = Key("Mise Run Settings") class MiseRunConfigurationSettingsEditor>( private val project: Project, ) : SettingsEditor() { + private val applicationState = application.service().state + private val useApplicationWideMiseConfig = applicationState.useMiseDirEnv + private val useApplicationWideMiseConfigPredicate = ComponentPredicate.fromValue(useApplicationWideMiseConfig) + private val myMiseDirEnvCb = JBCheckBox("Use environment variables from mise") private val myMiseConfigEnvironmentTf = JBTextField() - override fun createEditor(): JComponent { - val projectState = MiseSettings.getService(project).state - - val isOverridden = projectState.useMiseDirEnv - - myMiseDirEnvCb.selected.and(ComponentPredicate.fromValue(isOverridden.not())) - - return JPanel(BorderLayout()).apply { - add( - panel { - row { - cell(myMiseDirEnvCb) - .comment("Load environment variables from mise configuration file(s)") - }.enabled(isOverridden.not()) - row("Config Environment:") { - cell(myMiseConfigEnvironmentTf) - .comment( - """ - Specify the mise configuration environment to use (leave empty for default)
- Learn more about mise configuration environments - """.trimIndent(), - ).columns(COLUMNS_LARGE) - .focused() - .resizableColumn() - }.enabledIf(myMiseDirEnvCb.selected.and(ComponentPredicate.fromValue(isOverridden.not()))) - row { - icon(AllIcons.General.ShowWarning) - label("Using the configuration in Settings / Tools / Mise Settings") - .bold() - }.visible(isOverridden) - }, - ) + override fun createEditor(): JComponent = + panel { + row { + cell(myMiseDirEnvCb) + .comment("Load environment variables from mise configuration file(s)") + }.enabledIf(useApplicationWideMiseConfigPredicate.not()) + row("Config Environment:") { + cell(myMiseConfigEnvironmentTf) + .comment( + """ + Specify the mise configuration environment to use (leave empty for default)
+ Learn more about mise configuration environments + """.trimIndent(), + ).columns(COLUMNS_LARGE) + .focused() + .resizableColumn() + }.enabledIf(myMiseDirEnvCb.selected.and(useApplicationWideMiseConfigPredicate.not())) + row { + icon(AllIcons.General.ShowWarning) + label("Using the configuration in Settings / Tools / Mise Settings") + .bold() + }.visibleIf(useApplicationWideMiseConfigPredicate) } - } + // Write to persistence from the UI override fun applyEditorTo(config: T) { + val projectState = application.service().state + + // When the project is configured to use the project-wide mise configuration + if (projectState.useMiseDirEnv) { + // Ignore applying the settings to the run configuration + return + } + config.putCopyableUserData( USER_DATA_KEY, MiseRunConfigurationState( @@ -73,22 +75,26 @@ class MiseRunConfigurationSettingsEditor>( ) } + // Read from persistence to the UI override fun resetEditorFrom(config: T) { - val userData = config.getCopyableUserData(USER_DATA_KEY) ?: return - - val projectState = project.service().state - val state = userData.mergeProjectState(projectState) - - myMiseDirEnvCb.isSelected = state.useMiseDirEnv - myMiseConfigEnvironmentTf.text = state.miseConfigEnvironment - if (!myMiseDirEnvCb.isSelected) { - myMiseConfigEnvironmentTf.isEnabled = false + val runConfigurationState = config.getCopyableUserData(USER_DATA_KEY) ?: return + + val useMiseDirEnv: Boolean + val miseConfigEnvironment: String + + when (useApplicationWideMiseConfig) { + true -> { + useMiseDirEnv = applicationState.useMiseDirEnv + miseConfigEnvironment = applicationState.miseConfigEnvironment + } + false -> { + useMiseDirEnv = runConfigurationState.useMiseDirEnv + miseConfigEnvironment = runConfigurationState.miseConfigEnvironment + } } - if (projectState.useMiseDirEnv) { - myMiseDirEnvCb.isEnabled = false - myMiseConfigEnvironmentTf.isEnabled = false - } + myMiseDirEnvCb.isSelected = useMiseDirEnv + myMiseConfigEnvironmentTf.text = miseConfigEnvironment } companion object { diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/run/MiseRunConfigurationState.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/run/MiseRunConfigurationState.kt index b9e57842..8fbdebe4 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/run/MiseRunConfigurationState.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/run/MiseRunConfigurationState.kt @@ -1,7 +1,5 @@ package com.github.l34130.mise.core.run -import com.github.l34130.mise.core.setting.MiseSettings - data class MiseRunConfigurationState( var useMiseDirEnv: Boolean = true, var miseConfigEnvironment: String = "", @@ -11,10 +9,4 @@ data class MiseRunConfigurationState( useMiseDirEnv = useMiseDirEnv, miseConfigEnvironment = miseConfigEnvironment, ) - - fun mergeProjectState(projectState: MiseSettings.MyState): MiseRunConfigurationState = - this.copy( - useMiseDirEnv = projectState.useMiseDirEnv, - miseConfigEnvironment = projectState.miseConfigEnvironment, - ) } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/setting/MiseConfigurable.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/setting/MiseConfigurable.kt index 6008d63e..c68624a3 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/setting/MiseConfigurable.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/setting/MiseConfigurable.kt @@ -1,87 +1,89 @@ package com.github.l34130.mise.core.setting +import com.intellij.openapi.components.service import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.options.SearchableConfigurable import com.intellij.openapi.project.Project import com.intellij.ui.components.JBCheckBox import com.intellij.ui.components.JBTextField import com.intellij.ui.components.textFieldWithHistoryWithBrowseButton +import com.intellij.ui.dsl.builder.AlignX import com.intellij.ui.dsl.builder.COLUMNS_LARGE +import com.intellij.ui.dsl.builder.RowLayout import com.intellij.ui.dsl.builder.columns import com.intellij.ui.dsl.builder.panel import com.intellij.ui.layout.selected -import java.awt.BorderLayout +import com.intellij.util.application import javax.swing.JComponent -import javax.swing.JPanel class MiseConfigurable( private val project: Project, ) : SearchableConfigurable { + private val myMiseExecutableTf = + textFieldWithHistoryWithBrowseButton( + project = project, + browseDialogTitle = "Select Mise Executable", + fileChooserDescriptor = FileChooserDescriptor(true, false, false, false, false, false), + historyProvider = { listOf("/opt/homebrew/bin/mise").distinct() }, + ) private val myMiseDirEnvCb = JBCheckBox("Use environment variables from mise") private val myMiseConfigEnvironmentTf = JBTextField() override fun getDisplayName(): String = "Mise Settings" override fun createComponent(): JComponent { - val service = MiseSettings.getService(project) + val service = application.service() + myMiseExecutableTf.setTextAndAddToHistory(service.state.executablePath) myMiseDirEnvCb.isSelected = service.state.useMiseDirEnv myMiseConfigEnvironmentTf.text = service.state.miseConfigEnvironment - return JPanel(BorderLayout()).apply { - add( - panel { - row("Mise Executable:") { - cell( - textFieldWithHistoryWithBrowseButton( - project = project, - browseDialogTitle = "Select Mise Executable", - fileChooserDescriptor = FileChooserDescriptor(true, false, false, false, false, false), - historyProvider = { listOf("/opt/homebrew/bin/mise").distinct() }, - ).apply { - setTextAndAddToHistory(service.state.executablePath) - setTextFieldPreferredWidth(50) - } - ).comment( - """ - Specify the path to the mise executable.
- Not installed? Visit the mise installation - """.trimIndent() - ).resizableColumn() - } + return panel { + row("Mise Executable:") { + cell(myMiseExecutableTf) + .align(AlignX.FILL) + .comment( + """ + Specify the path to the mise executable.
+ Not installed? Visit the mise installation + """.trimIndent(), + ) + } - group("Environments") { - row { - cell(myMiseDirEnvCb).comment( - "Load environment variables from mise configuration file(s)", - ) - } - row("Config Environment:") { - cell(myMiseConfigEnvironmentTf) - .comment( - """ - Specify the mise configuration environment to use (leave empty for default)
- Learn more about mise configuration environments - """.trimIndent(), - ).columns(COLUMNS_LARGE) - .focused() - .resizableColumn() - }.enabledIf(myMiseDirEnvCb.selected) - } - }, - ) + groupRowsRange("Environments") { + row { + cell(myMiseDirEnvCb) + .align(AlignX.FILL) + .resizableColumn() + .comment("Load environment variables from mise configuration file(s)") + }.layout(RowLayout.PARENT_GRID) + row("Config Environment:") { + cell(myMiseConfigEnvironmentTf) + .columns(COLUMNS_LARGE) + .resizableColumn() + .comment( + """ + Specify the mise configuration environment to use (leave empty for default)
+ Learn more about mise configuration environments + """.trimIndent(), + ) + }.enabledIf(myMiseDirEnvCb.selected) + .layout(RowLayout.PARENT_GRID) + } } } override fun isModified(): Boolean { - val service = MiseSettings.getService(project) - return myMiseDirEnvCb.isSelected != service.state.useMiseDirEnv || - myMiseConfigEnvironmentTf.text != service.state.miseConfigEnvironment + val service = application.service() + return myMiseExecutableTf.text != service.state.executablePath || + myMiseDirEnvCb.isSelected != service.state.useMiseDirEnv || + myMiseConfigEnvironmentTf.text != service.state.miseConfigEnvironment } override fun apply() { if (isModified) { - val service = MiseSettings.getService(project) + val service = application.service() + service.state.executablePath = myMiseExecutableTf.text service.state.useMiseDirEnv = myMiseDirEnvCb.isSelected service.state.miseConfigEnvironment = myMiseConfigEnvironmentTf.text } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/setting/MiseSettings.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/setting/MiseSettings.kt index ee6412e6..73515cba 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/setting/MiseSettings.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/setting/MiseSettings.kt @@ -6,20 +6,14 @@ import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.Service import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage -import com.intellij.openapi.components.service import com.intellij.openapi.options.ShowSettingsUtil -import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.intellij.util.EnvironmentUtil import java.io.File - -@Service(Service.Level.PROJECT) +@Service(Service.Level.APP) @State(name = "com.github.l34130.mise.settings.MiseSettings", storages = [Storage("mise.xml")]) -class MiseSettings( - private val project: Project, -) : PersistentStateComponent { - +class MiseSettings : PersistentStateComponent { private var myState = MyState() override fun getState() = myState @@ -29,25 +23,28 @@ class MiseSettings( } override fun noStateLoaded() { - myState = MyState().also { - it.executablePath = getMiseExecutablePath() ?: "" - } + myState = + MyState().also { + it.executablePath = getMiseExecutablePath() ?: "" + } } override fun initializeComponent() { - myState = MyState().also { - it.executablePath = myState.executablePath.takeIf { it.isNotEmpty() } ?: getMiseExecutablePath() ?: "" - it.useMiseDirEnv = myState.useMiseDirEnv - it.miseConfigEnvironment = myState.miseConfigEnvironment - } + myState = + MyState().also { + it.executablePath = myState.executablePath.takeIf { it.isNotEmpty() } ?: getMiseExecutablePath() ?: "" + it.useMiseDirEnv = myState.useMiseDirEnv + it.miseConfigEnvironment = myState.miseConfigEnvironment + } if (myState.executablePath.isEmpty()) { - MiseNotificationService.getInstance(project).warn( + MiseNotificationService.getInstance(null).warn( title = "Mise Executable Not Found", - htmlText = """ + htmlText = + """ Mise executable not found in PATH.
Please specify the path to the mise executable in the settings. - """.trimIndent(), + """.trimIndent(), actionProvider = { NotificationAction.createSimple( "Open settings", @@ -60,8 +57,6 @@ class MiseSettings( } companion object { - fun getService(project: Project): MiseSettings = project.service() - private fun getMiseExecutablePath(): String? { val path = EnvironmentUtil.getValue("PATH") ?: return null @@ -81,12 +76,11 @@ class MiseSettings( var useMiseDirEnv: Boolean = true var miseConfigEnvironment: String = "" - public override fun clone(): MyState { - return MyState().also { + public override fun clone(): MyState = + MyState().also { it.executablePath = executablePath it.useMiseDirEnv = useMiseDirEnv it.miseConfigEnvironment = miseConfigEnvironment } - } } } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/setup/AbstractProjectSdkSetup.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/setup/AbstractProjectSdkSetup.kt index 13291b45..4e8ef078 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/setup/AbstractProjectSdkSetup.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/setup/AbstractProjectSdkSetup.kt @@ -10,6 +10,7 @@ import com.github.l34130.mise.core.setting.MiseSettings import com.github.l34130.mise.core.util.TerminalUtils import com.intellij.notification.NotificationAction import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.WriteAction import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable @@ -19,6 +20,7 @@ import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.util.io.FileUtil +import com.intellij.util.application import kotlin.reflect.KClass abstract class AbstractProjectSdkSetup : @@ -46,81 +48,84 @@ abstract class AbstractProjectSdkSetup : project: Project, isUserInteraction: Boolean, ) { - val devToolName = getDevToolName() - val miseNotificationService = project.service() + ApplicationManager.getApplication().executeOnPooledThread { + val devToolName = getDevToolName() + val miseNotificationService = project.service() - val configEnvironment = MiseSettings.getService(project).state.miseConfigEnvironment - val toolsResult = - MiseCommandLineHelper.getDevTools(workDir = project.basePath, configEnvironment = configEnvironment) - val tools = toolsResult.fold( - onSuccess = { tools -> tools[devToolName] }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load dev tools", it) - } - emptyList() - } - ) - - if (tools.isNullOrEmpty() || tools.size > 1) { - if (!isUserInteraction) return + val configEnvironment = application.service().state.miseConfigEnvironment + val toolsResult = + MiseCommandLineHelper.getDevTools(workDir = project.basePath, configEnvironment = configEnvironment) + val tools = + toolsResult.fold( + onSuccess = { tools -> tools[devToolName] }, + onFailure = { + if (it !is MiseCommandLineNotFoundException) { + MiseNotificationServiceUtils.notifyException("Failed to load dev tools", it) + } + emptyList() + }, + ) - val noOrMultiple = - if (tools.isNullOrEmpty()) { - "No" - } else { - "Multiple" - } + if (tools.isNullOrEmpty() || tools.size > 1) { + if (!isUserInteraction) return@executeOnPooledThread - miseNotificationService.warn( - "$noOrMultiple dev tools configuration for ${devToolName.canonicalName()} found", - "Check your Mise configuration or configure it manually", - ) { - NotificationAction.createSimple("Configure") { - val configurableClass = getConfigurableClass() - if (configurableClass != null) { - ShowSettingsUtil.getInstance().showSettingsDialog(project, configurableClass.javaObjectType) + val noOrMultiple = + if (tools.isNullOrEmpty()) { + "No" } else { - ShowSettingsUtil.getInstance().showSettingsDialog(project) + "Multiple" + } + + miseNotificationService.warn( + "$noOrMultiple dev tools configuration for ${devToolName.canonicalName()} found", + "Check your Mise configuration or configure it manually", + ) { + NotificationAction.createSimple("Configure") { + val configurableClass = getConfigurableClass() + if (configurableClass != null) { + ShowSettingsUtil.getInstance().showSettingsDialog(project, configurableClass.javaObjectType) + } else { + ShowSettingsUtil.getInstance().showSettingsDialog(project) + } } } - } - return - } + return@executeOnPooledThread + } - val tool = tools.first() + val tool = tools.first() - if (!tool.installed) { - miseNotificationService.warn( - "$devToolName@${tool.version} is not installed", - "Run `mise install` command to install the tool", - ) { - NotificationAction.createSimple("Run `mise install`") { - TerminalUtils.executeCommand( - project = project, - command = "mise install", - tabName = "mise install", - ) + if (!tool.installed) { + miseNotificationService.warn( + "$devToolName@${tool.version} is not installed", + "Run `mise install` command to install the tool", + ) { + NotificationAction.createSimple("Run `mise install`") { + TerminalUtils.executeCommand( + project = project, + command = "mise install", + tabName = "mise install", + ) + } } + return@executeOnPooledThread } - return - } - WriteAction.runAndWait { - try { - val updated = setupSdk(tool, project) - if (updated || isUserInteraction) { - miseNotificationService.info( - "${devToolName.canonicalName()} configured to $devToolName@${tool.version}", - tool.source?.absolutePath?.let(FileUtil::getLocationRelativeToUserHome) ?: "unknown source", + WriteAction.runAndWait { + try { + val updated = setupSdk(tool, project) + if (updated || isUserInteraction) { + miseNotificationService.info( + "${devToolName.canonicalName()} configured to ${devToolName.value}@${tool.version}", + tool.source?.absolutePath?.let(FileUtil::getLocationRelativeToUserHome) ?: "unknown source", + ) + } + } catch (e: Exception) { + miseNotificationService.error( + "Failed to set ${devToolName.canonicalName()} to ${devToolName.value}@${tool.version}", + e.message ?: e.javaClass.simpleName, ) } - } catch (e: Exception) { - miseNotificationService.error( - "Failed to set ${devToolName.canonicalName()} to $devToolName@${tool.version}", - e.message ?: e.javaClass.simpleName, - ) } } } diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseRootNode.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseRootNode.kt index 74cd912d..84b3a78a 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseRootNode.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseRootNode.kt @@ -8,7 +8,9 @@ import com.github.l34130.mise.core.notification.MiseNotificationServiceUtils import com.github.l34130.mise.core.setting.MiseSettings import com.intellij.ide.projectView.PresentationData import com.intellij.ide.util.treeView.AbstractTreeNode +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project +import com.intellij.util.application class MiseRootNode( private val nodeProject: Project, @@ -18,7 +20,7 @@ class MiseRootNode( } override fun getChildren(): Collection> { - val settings = MiseSettings.getService(nodeProject) + val settings = application.service() return listOf( MiseToolServiceNode(project, getToolNodes(settings)), @@ -28,18 +30,20 @@ class MiseRootNode( } private fun getToolNodes(settings: MiseSettings): Collection { - val toolsByToolNames = MiseCommandLineHelper.getDevTools( - workDir = nodeProject.basePath, - configEnvironment = settings.state.miseConfigEnvironment - ).fold( - onSuccess = { tools -> tools }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load dev tools", it) - } - emptyMap() - } - ) + val toolsByToolNames = + MiseCommandLineHelper + .getDevTools( + workDir = nodeProject.basePath, + configEnvironment = settings.state.miseConfigEnvironment, + ).fold( + onSuccess = { tools -> tools }, + onFailure = { + if (it !is MiseCommandLineNotFoundException) { + MiseNotificationServiceUtils.notifyException("Failed to load dev tools", it) + } + emptyMap() + }, + ) val toolsBySourcePaths = mutableMapOf>>() for ((toolName, toolInfos) in toolsByToolNames.entries) { @@ -60,18 +64,20 @@ class MiseRootNode( } private fun getEnvironmentNodes(settings: MiseSettings): Collection { - val envs = MiseCommandLineHelper.getEnvVars( - workDir = nodeProject.basePath, - configEnvironment = settings.state.miseConfigEnvironment - ).fold( - onSuccess = { envs -> envs }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) - } - emptyMap() - } - ) + val envs = + MiseCommandLineHelper + .getEnvVars( + workDir = nodeProject.basePath, + configEnvironment = settings.state.miseConfigEnvironment, + ).fold( + onSuccess = { envs -> envs }, + onFailure = { + if (it !is MiseCommandLineNotFoundException) { + MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) + } + emptyMap() + }, + ) return envs.map { (key, value) -> MiseEnvironmentNode( @@ -83,18 +89,20 @@ class MiseRootNode( } private fun getTaskNodes(settings: MiseSettings): Collection { - val tasks = MiseCommandLineHelper.getTasks( - workDir = nodeProject.basePath, - configEnvironment = settings.state.miseConfigEnvironment - ).fold( - onSuccess = { tasks -> tasks }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load tasks", it) - } - emptyList() - } - ) + val tasks = + MiseCommandLineHelper + .getTasks( + workDir = nodeProject.basePath, + configEnvironment = settings.state.miseConfigEnvironment, + ).fold( + onSuccess = { tasks -> tasks }, + onFailure = { + if (it !is MiseCommandLineNotFoundException) { + MiseNotificationServiceUtils.notifyException("Failed to load tasks", it) + } + emptyList() + }, + ) return tasks.map { task -> MiseTaskNode( diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseTaskNode.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseTaskNode.kt index 7e98b385..2bff5056 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseTaskNode.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseTaskNode.kt @@ -1,6 +1,6 @@ package com.github.l34130.mise.core.toolwindow.nodes -import com.github.l34130.mise.core.command.MiseRunTaskOnTerminalAction +import com.github.l34130.mise.core.action.MiseRunTaskOnTerminalAction import com.github.l34130.mise.core.command.MiseTask import com.github.l34130.mise.core.toolwindow.DoubleClickable import com.intellij.icons.AllIcons diff --git a/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseToolNode.kt b/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseToolNode.kt index aa760863..b41d535d 100644 --- a/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseToolNode.kt +++ b/modules/core/src/main/kotlin/com/github/l34130/mise/core/toolwindow/nodes/MiseToolNode.kt @@ -49,7 +49,7 @@ class MiseToolNode( toolInfo, AllIcons.General.Gear, ) { - override fun displayName(): String = "$toolName@${toolInfo.version}" + override fun displayName(): String = "${toolName.value}@${toolInfo.version}" override fun isActive(): Boolean = toolInfo.active diff --git a/modules/core/src/main/resources/schemas/mise-task.json b/modules/core/src/main/resources/schemas/mise-task.json new file mode 100644 index 00000000..4944b3e8 --- /dev/null +++ b/modules/core/src/main/resources/schemas/mise-task.json @@ -0,0 +1,238 @@ +{ + "$id": "https://mise.jdx.dev/schema/mise-task.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "mise-task-schema", + "type": "object", + "$defs": { + "task": { + "oneOf": [ + { + "description": "script to run", + "type": "string" + }, + { + "description": "script to run", + "items": { + "description": "script to run", + "type": "string" + }, + "type": "array" + }, + { + "additionalProperties": false, + "properties": { + "alias": { + "oneOf": [ + { + "description": "alias for this task", + "type": "string" + }, + { + "description": "alias for this task", + "items": { + "description": "alias for this task", + "type": "string" + }, + "type": "array" + } + ] + }, + "depends": { + "description": "other tasks to run before this task", + "items": { + "oneOf": [ + { + "description": "task with args to run before this task", + "type": "string" + }, + { + "description": "task with args to run before this task", + "items": { + "description": "task name and args", + "type": "string" + }, + "type": "array" + } + ] + }, + "type": "array" + }, + "depends_post": { + "description": "other tasks to run after this task", + "items": { + "oneOf": [ + { + "description": "task with args to run after this task", + "type": "string" + }, + { + "description": "task with args to run after this task", + "items": { + "description": "task name and args", + "type": "string" + }, + "type": "array" + } + ] + }, + "type": "array" + }, + "wait_for": { + "description": "if these tasks run, wait for them to complete first", + "items": { + "description": "task to run before this task", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "description of task", + "type": "string" + }, + "dir": { + "description": "directory to run script in, default is current working directory", + "type": "string" + }, + "env": { + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "enum": [false], + "type": "boolean" + } + ] + }, + "description": "environment variables", + "type": "object" + }, + "tools": { + "description": "tools to install/activate before running this task", + "additionalProperties": { + "oneOf": [ + { + "description": "version of the tool to install", + "type": "string" + }, + { + "properties": { + "version": { + "description": "version of the tool to install", + "type": "string" + }, + "os": { + "oneOf": [ + { + "description": "operating system to install on", + "type": "array" + }, + { + "description": "option to pass to tool", + "type": "string" + }, + { + "description": "option to pass to tool", + "type": "boolean" + } + ] + } + }, + "required": ["version"], + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + } + ] + } + } + ] + }, + "type": "object" + }, + "hide": { + "description": "do not display this task", + "type": "boolean" + }, + "outputs": { + "description": "files created by this task", + "items": { + "description": "glob pattern or path to files created by this task", + "type": "string" + }, + "type": "array" + }, + "quiet": { + "description": "do not display mise information for this task", + "type": "boolean" + }, + "raw": { + "description": "directly connect task to stdin/stdout/stderr", + "type": "boolean" + }, + "run": { + "oneOf": [ + { + "description": "script to run", + "type": "string" + }, + { + "description": "script to run", + "items": { + "description": "script to run", + "type": "string" + }, + "type": "array" + } + ] + }, + "run_windows": { + "oneOf": [ + { + "description": "script to run on windows", + "type": "string" + }, + { + "description": "script to run on windows", + "items": { + "description": "script to run on windows", + "type": "string" + }, + "type": "array" + } + ] + }, + "file": { + "description": "Execute an external script", + "type": "string" + }, + "sources": { + "description": "files that this task depends on", + "items": { + "description": "glob pattern or path to files that this task depends on", + "type": "string" + }, + "type": "array" + }, + "shell": { + "description": "specify a shell command to run the script with", + "type": "string", + "default": "sh -c" + } + }, + "type": "object" + } + ] + } + }, + "description": "Config file for included mise tasks (https://mise.jdx.dev/tasks/#task-configuration)", + "additionalProperties": { + "$ref": "#/$defs/task" + } +} diff --git a/modules/core/src/main/resources/schemas/mise.json b/modules/core/src/main/resources/schemas/mise.json new file mode 100644 index 00000000..82107f90 --- /dev/null +++ b/modules/core/src/main/resources/schemas/mise.json @@ -0,0 +1,1457 @@ +{ + "$id": "https://mise.jdx.dev/schema/mise.json", + "$schema": "http://json-schema.org/2019-09/schema#", + "title": "mise", + "type": "object", + "$defs": { + "env": { + "additionalProperties": { + "oneOf": [ + { + "type": "object", + "properties": { + "value": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "tools": { + "type": "boolean", + "description": "load tools before resolving" + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + }, + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + }, + "description": "environment variables", + "properties": { + "_": { + "description": "environment modules", + "additionalProperties": true, + "properties": { + "file": { + "oneOf": [ + { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "tools": { + "type": "boolean", + "description": "load tools before resolving" + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + }, + { + "description": "dotenv file to load", + "type": "string" + }, + { + "description": "dotenv files to load", + "items": { + "description": "dotenv file to load", + "type": "string" + }, + "type": "array" + } + ] + }, + "path": { + "oneOf": [ + { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + }, + { + "description": "PATH entry to add", + "type": "string" + }, + { + "description": "PATH entries to add", + "items": { + "description": "PATH entry to add", + "type": "string" + }, + "type": "array" + } + ] + }, + "python": { + "description": "python environment", + "properties": { + "venv": { + "oneOf": [ + { + "description": "path to python virtual environment to use", + "type": "string" + }, + { + "description": "virtualenv options", + "properties": { + "create": { + "default": false, + "description": "create a new virtual environment if one does not exist", + "type": "boolean" + }, + "path": { + "description": "path to python virtual environment to use", + "type": "string" + }, + "python": { + "description": "python version to use", + "type": "string" + }, + "python_create_args": { + "description": "additional arguments to pass to python when creating a virtual environment", + "type": "string" + }, + "uv_create_args": { + "description": "additional arguments to pass to uv when creating a virtual environment", + "type": "string" + } + }, + "required": ["path"], + "type": "object" + } + ] + } + }, + "type": "object" + }, + "source": { + "oneOf": [ + { + "type": "object", + "properties": { + "path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "tools": { + "type": "boolean", + "description": "load tools before resolving" + }, + "redact": { + "type": "boolean", + "description": "redact the value from logs" + } + } + }, + { + "description": "bash script to load", + "type": "string" + }, + { + "description": "bash scripts to load", + "items": { + "description": "bash script to load", + "type": "string" + }, + "type": "array" + } + ] + } + }, + "type": "object" + } + }, + "type": "object" + }, + "settings": { + "properties": { + "activate_aggressive": { + "description": "Pushes tools' bin-paths to the front of PATH instead of allowing modifications of PATH after activation to take precedence.", + "type": "boolean" + }, + "all_compile": { + "description": "do not use precompiled binaries for any tool", + "type": "boolean" + }, + "always_keep_download": { + "description": "should mise keep downloaded files after installation", + "type": "boolean" + }, + "always_keep_install": { + "description": "should mise keep install files after installation even if the installation fails", + "type": "boolean" + }, + "aqua": { + "additionalProperties": false, + "properties": { + "cosign": { + "default": true, + "description": "Use cosign to verify aqua tool signatures.", + "type": "boolean" + }, + "minisign": { + "default": true, + "description": "Use minisign to verify aqua tool signatures.", + "type": "boolean" + }, + "registry_url": { + "description": "URL to fetch aqua registry from.", + "type": "string" + }, + "slsa": { + "default": true, + "description": "Use SLSA to verify aqua tool signatures.", + "type": "boolean" + } + } + }, + "arch": { + "description": "Architecture to use for precompiled binaries.", + "type": "string" + }, + "asdf": { + "description": "use asdf as a default plugin backend", + "type": "boolean", + "deprecated": true + }, + "asdf_compat": { + "description": "set to true to ensure .tool-versions will be compatible with asdf", + "type": "boolean", + "deprecated": true + }, + "auto_install": { + "default": true, + "description": "Automatically install missing tools when running `mise x`, `mise run`, or as part of the 'not found' handler.", + "type": "boolean" + }, + "auto_install_disable_tools": { + "description": "List of tools to skip automatically installing when running `mise x`, `mise run`, or as part of the 'not found' handler.", + "type": "array", + "items": { + "type": "string" + } + }, + "cache_prune_age": { + "default": "30d", + "description": "Delete files in cache that have not been accessed in this duration", + "type": "string" + }, + "cargo": { + "additionalProperties": false, + "properties": { + "binstall": { + "default": true, + "description": "Use cargo-binstall instead of cargo install if available", + "type": "boolean" + } + } + }, + "cargo_binstall": { + "description": "Use cargo-binstall instead of cargo install if available", + "type": "boolean", + "deprecated": true + }, + "cd": { + "description": "Path to change to after launching mise", + "type": "string" + }, + "ci": { + "description": "Set to true if running in a CI environment", + "type": "boolean" + }, + "color": { + "default": true, + "description": "Use color in mise terminal output", + "type": "boolean" + }, + "debug": { + "description": "Sets log level to debug", + "type": "boolean" + }, + "default_config_filename": { + "default": "mise.toml", + "description": "The default config filename read. `mise use` and other commands that create new config files will use this value. This must be an env var.", + "type": "string" + }, + "default_tool_versions_filename": { + "default": ".tool-versions", + "description": "The default .tool-versions filename read. This will not ignore .tool-versions—use override_tool_versions_filename for that. This must be an env var.", + "type": "string" + }, + "disable_backends": { + "default": [], + "description": "Backends to disable such as `asdf` or `pipx`", + "type": "array", + "items": { + "type": "string" + } + }, + "disable_default_registry": { + "description": "Disable the default mapping of short tool names like `go` -> `vfox:version-fox/vfox-golang`", + "type": "boolean" + }, + "disable_default_shorthands": { + "description": "Disables built-in shorthands to asdf/vfox plugins", + "type": "boolean", + "deprecated": true + }, + "disable_hints": { + "default": [], + "description": "Turns off helpful hints when using different mise features", + "type": "array", + "items": { + "type": "string" + } + }, + "disable_tools": { + "default": [], + "description": "Tools defined in mise.toml that should be ignored", + "type": "array", + "items": { + "type": "string" + } + }, + "dotnet": { + "additionalProperties": false, + "properties": { + "package_flags": { + "default": [], + "description": "Extends dotnet search and install abilities.", + "type": "array", + "items": { + "type": "string" + } + }, + "registry_url": { + "default": "https://api.nuget.org/v3/index.json", + "description": "URL to fetch dotnet tools from.", + "type": "string" + } + } + }, + "env": { + "default": [], + "description": "Env to use for mise..toml files.", + "type": "array", + "items": { + "type": "string" + } + }, + "env_file": { + "description": "Path to a file containing environment variables to automatically load.", + "type": "string" + }, + "erlang": { + "additionalProperties": false, + "properties": { + "compile": { + "description": "If true, compile erlang from source. If false, use precompiled binaries. If not set, use precompiled binaries if available.", + "type": "boolean" + } + } + }, + "exec_auto_install": { + "default": true, + "description": "Automatically install missing tools when running `mise x`.", + "type": "boolean" + }, + "experimental": { + "description": "Enable experimental mise features which are incomplete or unstable—breakings changes may occur", + "type": "boolean" + }, + "fetch_remote_versions_cache": { + "default": "1h", + "description": "How long to cache remote versions for tools.", + "type": "string" + }, + "fetch_remote_versions_timeout": { + "default": "5s", + "description": "Timeout in seconds for HTTP requests to fetch new tool versions in mise.", + "type": "string" + }, + "global_config_file": { + "description": "Path to the global mise config file. Default is `~/.config/mise/config.toml`. This must be an env var.", + "type": "string" + }, + "global_config_root": { + "description": "Path which is used as `{{config_root}}` for the global config file. Default is `$HOME`. This must be an env var.", + "type": "string" + }, + "go_default_packages_file": { + "default": "~/.default-go-packages", + "description": "Path to a file containing default go packages to install when installing go", + "type": "string" + }, + "go_download_mirror": { + "default": "https://dl.google.com/go", + "description": "Mirror to download go sdk tarballs from.", + "type": "string" + }, + "go_repo": { + "default": "https://github.com/golang/go", + "description": "URL to fetch go from.", + "type": "string" + }, + "go_set_gobin": { + "description": "Changes where `go install` installs binaries to.", + "type": "boolean" + }, + "go_set_gopath": { + "description": "[deprecated] Set to true to set GOPATH=~/.local/share/mise/installs/go/.../packages.", + "type": "boolean", + "deprecated": true + }, + "go_set_goroot": { + "default": true, + "description": "Sets GOROOT=~/.local/share/mise/installs/go/.../.", + "type": "boolean" + }, + "go_skip_checksum": { + "description": "Set to true to skip checksum verification when downloading go sdk tarballs.", + "type": "boolean" + }, + "http_timeout": { + "default": "30s", + "description": "Timeout in seconds for all HTTP requests in mise.", + "type": "string" + }, + "idiomatic_version_file": { + "default": true, + "description": "Set to false to disable the idiomatic version files such as .node-version, .ruby-version, etc.", + "type": "boolean" + }, + "idiomatic_version_file_disable_tools": { + "default": [], + "description": "Specific tools to disable idiomatic version files for.", + "type": "array", + "items": { + "type": "string" + } + }, + "ignored_config_paths": { + "default": [], + "description": "This is a list of config paths that mise will ignore.", + "type": "array", + "items": { + "type": "string" + } + }, + "jobs": { + "default": 8, + "description": "How many jobs to run concurrently such as tool installs.", + "type": "number" + }, + "legacy_version_file": { + "default": true, + "description": "Set to false to disable the idiomatic version files such as .node-version, .ruby-version, etc.", + "type": "boolean", + "deprecated": true + }, + "legacy_version_file_disable_tools": { + "default": [], + "description": "Specific tools to disable idiomatic version files for.", + "type": "array", + "deprecated": true, + "items": { + "type": "string" + } + }, + "libgit2": { + "default": true, + "description": "Use libgit2 for git operations, set to false to shell out to git.", + "type": "boolean" + }, + "lockfile": { + "default": true, + "description": "Create and read lockfiles for tool versions.", + "type": "boolean" + }, + "log_level": { + "default": "info", + "description": "Show more/less output.", + "type": "string", + "enum": ["trace", "debug", "info", "warn", "error"] + }, + "node": { + "additionalProperties": false, + "properties": { + "compile": { + "description": "Compile node from source.", + "type": "boolean" + }, + "flavor": { + "description": "Install a specific node flavor like glibc-217 or musl. Use with unofficial node build repo.", + "type": "string" + }, + "gpg_verify": { + "description": "Use gpg to verify node tool signatures.", + "type": "boolean" + }, + "mirror_url": { + "description": "Mirror to download node tarballs from.", + "type": "string" + } + } + }, + "not_found_auto_install": { + "default": true, + "description": "Set to false to disable the \"command not found\" handler to autoinstall missing tool versions.", + "type": "boolean" + }, + "npm": { + "additionalProperties": false, + "properties": { + "bun": { + "description": "Use bun instead of npm if bun is installed and on PATH.", + "type": "boolean" + } + } + }, + "override_config_filenames": { + "default": [], + "description": "If set, mise will ignore default config files like `mise.toml` and use these filenames instead. This must be an env var.", + "type": "array", + "items": { + "type": "string" + } + }, + "override_tool_versions_filename": { + "default": [], + "description": "If set, mise will ignore .tool-versions files and use these filename instead. Can be set to `none` to disable .tool-versions. This must be an env var.", + "type": "array", + "items": { + "type": "string" + } + }, + "paranoid": { + "description": "Enables extra-secure behavior.", + "type": "boolean" + }, + "pin": { + "description": "Default to pinning versions when running `mise use` in mise.toml files.", + "type": "boolean" + }, + "pipx": { + "additionalProperties": false, + "properties": { + "uvx": { + "description": "Use uvx instead of pipx if uv is installed and on PATH.", + "type": "boolean" + } + } + }, + "pipx_uvx": { + "description": "Use uvx instead of pipx if uv is installed and on PATH.", + "type": "boolean" + }, + "plugin_autoupdate_last_check_duration": { + "default": "7d", + "description": "How long to wait before updating plugins automatically (note this isn't currently implemented).", + "type": "string" + }, + "profile": { + "description": "Profile to use for mise.${MISE_PROFILE}.toml files.", + "type": "string", + "deprecated": true + }, + "python": { + "additionalProperties": false, + "properties": { + "compile": { + "description": "If true, compile python from source. If false, use precompiled binaries. If not set, use precompiled binaries if available.", + "type": "boolean" + }, + "default_packages_file": { + "description": "Path to a file containing default python packages to install when installing a python version.", + "type": "string" + }, + "patch_url": { + "description": "URL to fetch python patches from to pass to python-build.", + "type": "string" + }, + "patches_directory": { + "description": "Directory to fetch python patches from.", + "type": "string" + }, + "precompiled_arch": { + "description": "Specify the architecture to use for precompiled binaries.", + "type": "string" + }, + "precompiled_flavor": { + "description": "Specify the flavor to use for precompiled binaries.", + "type": "string" + }, + "precompiled_os": { + "description": "Specify the OS to use for precompiled binaries.", + "type": "string" + }, + "pyenv_repo": { + "default": "https://github.com/pyenv/pyenv.git", + "description": "URL to fetch pyenv from for compiling python with python-build.", + "type": "string" + }, + "uv_venv_auto": { + "description": "Integrate with uv to automatically create/source venvs if uv.lock is present.", + "type": "boolean" + }, + "uv_venv_create_args": { + "description": "Arguments to pass to uv when creating a venv.", + "type": "array", + "items": { + "type": "string" + } + }, + "venv_auto_create": { + "description": "Automatically create virtualenvs for python tools.", + "type": "boolean", + "deprecated": true + }, + "venv_create_args": { + "description": "Arguments to pass to python when creating a venv. (not used for uv venv creation)", + "type": "array", + "items": { + "type": "string" + } + }, + "venv_stdlib": { + "description": "Prefer to use venv from Python's standard library.", + "type": "boolean" + } + } + }, + "python_compile": { + "description": "If true, compile python from source. If false, use precompiled binaries. If not set, use precompiled binaries if available.", + "type": "boolean", + "deprecated": true + }, + "python_default_packages_file": { + "description": "Path to a file containing default python packages to install when installing python.", + "type": "string", + "deprecated": true + }, + "python_patch_url": { + "description": "URL to fetch python patches from.", + "type": "string", + "deprecated": true + }, + "python_patches_directory": { + "description": "Directory to fetch python patches from.", + "type": "string", + "deprecated": true + }, + "python_precompiled_arch": { + "description": "Specify the architecture to use for precompiled binaries.", + "type": "string", + "deprecated": true + }, + "python_precompiled_os": { + "description": "Specify the OS to use for precompiled binaries.", + "type": "string", + "deprecated": true + }, + "python_pyenv_repo": { + "description": "URL to fetch pyenv from for compiling python.", + "type": "string", + "deprecated": true + }, + "python_venv_auto_create": { + "description": "Automatically create virtualenvs for python tools.", + "type": "boolean", + "deprecated": true + }, + "python_venv_stdlib": { + "description": "Prefer to use venv from Python's standard library.", + "type": "boolean", + "deprecated": true + }, + "quiet": { + "description": "Suppress all output except errors.", + "type": "boolean" + }, + "raw": { + "description": "Connect stdin/stdout/stderr to child processes.", + "type": "boolean" + }, + "ruby": { + "additionalProperties": false, + "properties": { + "apply_patches": { + "description": "A list of patch files or URLs to apply to ruby source.", + "type": "string" + }, + "default_packages_file": { + "default": "~/.default-gems", + "description": "Path to a file containing default ruby gems to install when installing ruby.", + "type": "string" + }, + "ruby_build_opts": { + "description": "Options to pass to ruby-build.", + "type": "string" + }, + "ruby_build_repo": { + "default": "https://github.com/rbenv/ruby-build.git", + "description": "URL to fetch ruby-build from.", + "type": "string" + }, + "ruby_install": { + "description": "Use ruby-install instead of ruby-build.", + "type": "boolean" + }, + "ruby_install_opts": { + "description": "Options to pass to ruby-install.", + "type": "string" + }, + "ruby_install_repo": { + "default": "https://github.com/postmodern/ruby-install.git", + "description": "URL to fetch ruby-install from.", + "type": "string" + }, + "verbose_install": { + "description": "Set to true to enable verbose output during ruby installation.", + "type": "boolean" + } + } + }, + "rust": { + "additionalProperties": false, + "properties": { + "cargo_home": { + "description": "Path to the cargo home directory. Defaults to ~/.cargo or %USERPROFILE%\\.cargo", + "type": "string" + }, + "rustup_home": { + "description": "Path to the rustup home directory. Defaults to ~/.rustup or %USERPROFILE%\\.rustup", + "type": "string" + } + } + }, + "shorthands_file": { + "description": "Path to a file containing custom tool shorthands.", + "type": "string" + }, + "silent": { + "description": "Suppress all `mise run|watch` output except errors—including what tasks output.", + "type": "boolean" + }, + "sops": { + "additionalProperties": false, + "properties": { + "age_key": { + "description": "The age private key to use for sops secret decryption.", + "type": "string" + }, + "age_key_file": { + "description": "Path to the age private key file to use for sops secret decryption.", + "type": "string" + }, + "age_recipients": { + "description": "The age public keys to use for sops secret encryption.", + "type": "string" + }, + "rops": { + "default": true, + "description": "Use rops to decrypt sops files. Disable to shell out to `sops` which will slow down mise but sops may offer features not available in rops.", + "type": "boolean" + } + } + }, + "status": { + "additionalProperties": false, + "properties": { + "missing_tools": { + "default": "if_other_versions_installed", + "description": "Show a warning if tools are not installed when entering a directory with a mise.toml file.", + "type": "string" + }, + "show_env": { + "description": "Show configured env vars when entering a directory with a mise.toml file.", + "type": "boolean" + }, + "show_tools": { + "description": "Show configured env vars when entering a directory with a mise.toml file.", + "type": "boolean" + } + } + }, + "swift": { + "additionalProperties": false, + "properties": { + "gpg_verify": { + "description": "Use gpg to verify swift tool signatures.", + "type": "boolean" + }, + "platform": { + "description": "Override the platform to use for precompiled binaries.", + "type": "string" + } + } + }, + "system_config_file": { + "description": "Path to the system mise config file. Default is `/etc/mise/config.toml`. This must be an env var.", + "type": "string" + }, + "task_disable_paths": { + "default": [], + "description": "Paths that mise will not look for tasks in.", + "type": "array", + "items": { + "type": "string" + } + }, + "task_output": { + "description": "Change output style when executing tasks.", + "type": "string", + "enum": [ + "prefix", + "interleave", + "keep-order", + "replacing", + "timed", + "quiet", + "silent" + ] + }, + "task_run_auto_install": { + "default": true, + "description": "Automatically install missing tools when executing tasks.", + "type": "boolean" + }, + "task_skip": { + "default": [], + "description": "Tasks to skip when running `mise run`.", + "type": "array", + "items": { + "type": "string" + } + }, + "task_timings": { + "description": "Show completion message with elapsed time for each task on `mise run`. Default shows when output type is `prefix`.", + "type": "boolean" + }, + "trace": { + "description": "Sets log level to trace", + "type": "boolean" + }, + "trusted_config_paths": { + "default": [], + "description": "This is a list of config paths that mise will automatically mark as trusted.", + "type": "array", + "items": { + "type": "string" + } + }, + "unix_default_file_shell_args": { + "default": "sh", + "description": "List of default shell arguments for unix to be used with `file`. For example `sh`.", + "type": "string" + }, + "unix_default_inline_shell_args": { + "default": "sh -c -o errexit", + "description": "List of default shell arguments for unix to be used with inline commands. For example, `sh`, `-c` for sh.", + "type": "string" + }, + "use_file_shell_for_executable_tasks": { + "default": false, + "description": "Determines whether to use a specified shell for executing tasks in the tasks directory. When set to true, the shell defined in the file will be used, or the default shell specified by `windows_default_file_shell_args` or `unix_default_file_shell_args` will be applied. If set to false, tasks will be executed directly as programs.", + "type": "boolean" + }, + "use_versions_host": { + "default": true, + "description": "Set to false to disable using the mise-versions API as a quick way for mise to query for new versions.", + "type": "boolean" + }, + "verbose": { + "description": "Shows more verbose output such as installation logs when installing tools.", + "type": "boolean" + }, + "vfox": { + "description": "Use vfox as a default plugin backend instead of asdf.", + "type": "boolean", + "deprecated": true + }, + "windows_default_file_shell_args": { + "default": "cmd /c", + "description": "List of default shell arguments for Windows to be used for file commands. For example, `cmd`, `/c` for cmd.exe.", + "type": "array", + "items": { + "type": "string" + } + }, + "windows_default_inline_shell_args": { + "default": "cmd /c", + "description": "List of default shell arguments for Windows to be used for inline commands. For example, `cmd`, `/c` for cmd.exe.", + "type": "string" + }, + "windows_executable_extensions": { + "default": ["exe", "bat", "cmd", "com", "ps1", "vbs"], + "description": "List of executable extensions for Windows. For example, `exe` for .exe files, `bat` for .bat files, and so on.", + "type": "array", + "items": { + "type": "string" + } + }, + "yes": { + "description": "This will automatically answer yes or no to prompts. This is useful for scripting.", + "type": "boolean" + } + } + }, + "task": { + "oneOf": [ + { + "description": "script to run", + "type": "string" + }, + { + "description": "script to run", + "items": { + "description": "script to run", + "type": "string" + }, + "type": "array" + }, + { + "additionalProperties": false, + "properties": { + "alias": { + "oneOf": [ + { + "description": "alias for this task", + "type": "string" + }, + { + "description": "alias for this task", + "items": { + "description": "alias for this task", + "type": "string" + }, + "type": "array" + } + ] + }, + "depends": { + "description": "other tasks to run before this task", + "items": { + "oneOf": [ + { + "description": "task with args to run before this task", + "type": "string" + }, + { + "description": "task with args to run before this task", + "items": { + "description": "task name and args", + "type": "string" + }, + "type": "array" + } + ] + }, + "type": "array" + }, + "depends_post": { + "description": "other tasks to run after this task", + "items": { + "oneOf": [ + { + "description": "task with args to run after this task", + "type": "string" + }, + { + "description": "task with args to run after this task", + "items": { + "description": "task name and args", + "type": "string" + }, + "type": "array" + } + ] + }, + "type": "array" + }, + "wait_for": { + "description": "if these tasks run, wait for them to complete first", + "items": { + "description": "task to run before this task", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "description of task", + "type": "string" + }, + "dir": { + "description": "directory to run script in, default is current working directory", + "type": "string" + }, + "env": { + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "enum": [false], + "type": "boolean" + } + ] + }, + "description": "environment variables", + "type": "object" + }, + "tools": { + "description": "tools to install/activate before running this task", + "additionalProperties": { + "oneOf": [ + { + "description": "version of the tool to install", + "type": "string" + }, + { + "properties": { + "version": { + "description": "version of the tool to install", + "type": "string" + }, + "os": { + "oneOf": [ + { + "description": "operating system to install on", + "type": "array" + }, + { + "description": "option to pass to tool", + "type": "string" + }, + { + "description": "option to pass to tool", + "type": "boolean" + } + ] + } + }, + "required": ["version"], + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + } + ] + } + } + ] + }, + "type": "object" + }, + "hide": { + "description": "do not display this task", + "type": "boolean" + }, + "outputs": { + "description": "files created by this task", + "items": { + "description": "glob pattern or path to files created by this task", + "type": "string" + }, + "type": "array" + }, + "quiet": { + "description": "do not display mise information for this task", + "type": "boolean" + }, + "raw": { + "description": "directly connect task to stdin/stdout/stderr", + "type": "boolean" + }, + "run": { + "oneOf": [ + { + "description": "script to run", + "type": "string" + }, + { + "description": "script to run", + "items": { + "description": "script to run", + "type": "string" + }, + "type": "array" + } + ] + }, + "run_windows": { + "oneOf": [ + { + "description": "script to run on windows", + "type": "string" + }, + { + "description": "script to run on windows", + "items": { + "description": "script to run on windows", + "type": "string" + }, + "type": "array" + } + ] + }, + "file": { + "description": "Execute an external script", + "type": "string" + }, + "sources": { + "description": "files that this task depends on", + "items": { + "description": "glob pattern or path to files that this task depends on", + "type": "string" + }, + "type": "array" + }, + "shell": { + "description": "specify a shell command to run the script with", + "type": "string", + "default": "sh -c" + }, + "usage": { + "description": "Specify usage (https://usage.jdx.dev/) specs for the task", + "type": "string" + } + }, + "type": "object" + } + ] + }, + "vars": { + "description": "variables to set", + "type": "object", + "properties": { + "_": { + "description": "vars modules", + "additionalProperties": true, + "properties": { + "file": { + "oneOf": [ + { + "description": "dotenv file to load", + "type": "string" + }, + { + "description": "dotenv files to load", + "items": { + "description": "dotenv file to load", + "type": "string" + }, + "type": "array" + } + ] + }, + "source": { + "oneOf": [ + { + "description": "bash script to load", + "type": "string" + }, + { + "description": "bash scripts to load", + "items": { + "description": "bash script to load", + "type": "string" + }, + "type": "array" + } + ] + } + }, + "type": "object" + } + }, + "additionalProperties": { + "description": "value of variable", + "type": "string" + } + }, + "task_config": { + "description": "configuration for task execution/management", + "type": "object", + "additionalProperties": false, + "properties": { + "dir": { + "description": "default directory to run tasks in defined in this file", + "type": "string" + }, + "includes": { + "description": "files/directories to include searching for tasks", + "items": { + "description": "file/directory root to include in task execution", + "type": "string" + }, + "type": "array" + } + } + }, + "tool": { + "oneOf": [ + { + "description": "version of the tool to install", + "type": "string" + }, + { + "properties": { + "version": { + "description": "version of the tool to install", + "type": "string" + }, + "os": { + "oneOf": [ + { + "description": "operating system to install on", + "type": "array" + }, + { + "description": "option to pass to tool", + "type": "string" + }, + { + "description": "option to pass to tool", + "type": "boolean" + } + ] + } + }, + "required": ["version"], + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + } + ] + } + } + ] + }, + "hooks": { + "description": "hooks to run", + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "description": "script to run", + "type": "string" + }, + { + "description": "script to run", + "items": { + "description": "script to run", + "type": "string" + }, + "type": "array" + }, + { + "additionalProperties": false, + "properties": { + "script": { + "description": "script to run", + "type": "string" + }, + "shell": { + "description": "specify the shell to run the script inside of", + "type": "string" + } + }, + "type": "object" + } + ] + } + }, + "watch_files": { + "description": "files to watch for changes", + "type": "array", + "items": { + "type": "object", + "description": "file to watch for changes", + "additionalProperties": false, + "properties": { + "run": { + "type": "string", + "description": "script to run when file changes", + "items": { + "type": "string" + } + }, + "patterns": { + "type": "array", + "description": "patterns to watch for", + "items": { + "type": "string" + } + } + } + } + } + }, + "additionalProperties": false, + "description": "config file for mise version manager (mise.toml)", + "properties": { + "alias": { + "description": "custom shorthands", + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "description": "where the alias goes", + "type": "string" + }, + { + "description": "tool to set aliases for", + "type": "object", + "additionalProperties": { + "description": "version alias points to", + "type": "string" + } + } + ] + } + }, + "env": { + "oneOf": [ + { + "$ref": "#/$defs/env" + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/env" + } + } + ] + }, + "min_version": { + "description": "minimum version of mise required to use this config", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "type": "string" + }, + "redactions": { + "description": "env or vars keys to redact from logs", + "type": "array", + "items": { + "type": "string" + } + }, + "plugins": { + "additionalProperties": { + "description": "url to plugin repository", + "type": "string" + }, + "description": "plugins to use", + "type": "object" + }, + "settings": { + "$ref": "#/$defs/settings", + "additionalProperties": false, + "description": "mise settings", + "type": "object" + }, + "task_config": { + "$ref": "#/$defs/task_config" + }, + "tasks": { + "additionalProperties": { + "$ref": "#/$defs/task" + }, + "description": "task runner tasks", + "type": "object" + }, + "tools": { + "additionalProperties": { + "oneOf": [ + { + "items": { + "$ref": "#/$defs/tool" + }, + "type": "array" + }, + { + "$ref": "#/$defs/tool" + } + ] + }, + "description": "dev tools to use", + "type": "object" + }, + "hooks": { + "$ref": "#/$defs/hooks" + }, + "vars": { + "$ref": "#/$defs/vars" + }, + "watch_files": { + "$ref": "#/$defs/watch_files" + }, + "_": { + "additionalProperties": true + } + } +} diff --git a/modules/core/src/test/kotlin/com/github/l34130/mise/core/MiseTomlTestBase.kt b/modules/core/src/test/kotlin/com/github/l34130/mise/core/MiseTomlTestBase.kt new file mode 100644 index 00000000..9102af50 --- /dev/null +++ b/modules/core/src/test/kotlin/com/github/l34130/mise/core/MiseTomlTestBase.kt @@ -0,0 +1,104 @@ +package com.github.l34130.mise.core + +import com.intellij.injected.editor.VirtualFileWindow +import com.intellij.lang.LanguageCommenters +import com.intellij.lang.injection.InjectedLanguageManager +import com.intellij.openapi.editor.Document +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import kotlin.math.min + +abstract class MiseTomlTestBase : BasePlatformTestCase() { + protected fun InlineFile( + text: String, + fileName: String = "mise.toml", + ): PsiFile = myFixture.configureByText(fileName, text.trimIndent()) + + protected inline fun findElementInEditor(marker: String = "^"): T = findElementInEditor(T::class.java, marker) + + protected fun findElementInEditor( + psiClass: Class, + marker: String, + ): T { + val (element, data) = findElementWithDataAndOffsetInEditor(psiClass, marker) + check(data.isEmpty()) { "Did not expect marker data" } + return element + } + + protected inline fun findElementAndDataInEditor(marker: String = "^"): Pair { + val (element, data) = findElementWithDataAndOffsetInEditor(marker) + return element to data + } + + protected inline fun findElementWithDataAndOffsetInEditor(marker: String = "^"): Triple = + findElementWithDataAndOffsetInEditor(T::class.java, marker) + + protected fun findElementWithDataAndOffsetInEditor( + psiClass: Class, + marker: String, + ): Triple { + val elementsWithDataAndOffset = findElementsWithDataAndOffsetInEditor(psiClass, marker) + check(elementsWithDataAndOffset.isNotEmpty()) { "No `$marker` marker:\n${myFixture.file.text}" } + check(elementsWithDataAndOffset.size <= 1) { "More than one `$marker` marker:\n${myFixture.file.text}" } + return elementsWithDataAndOffset.first() + } + + protected fun findElementsWithDataAndOffsetInEditor( + psiClass: Class, + marker: String, + ): List> = + findElementsWithDataAndOffsetInEditor( + myFixture.file, + myFixture.file.viewProvider.document!!, + psiClass, + marker, + ) + + protected fun findElementsWithDataAndOffsetInEditor( + file: PsiFile, + doc: Document, + psiClass: Class, + marker: String, + ): List> { + val commentPrefix = LanguageCommenters.INSTANCE.forLanguage(file.language).lineCommentPrefix ?: "//" + val caretMarker = "$commentPrefix$marker" + val text = file.text + val result = mutableListOf>() + var markerOffset = -caretMarker.length + while (true) { + markerOffset = text.indexOf(caretMarker, markerOffset + caretMarker.length) + if (markerOffset == -1) break + val data = + text + .drop(markerOffset) + .removePrefix(caretMarker) + .takeWhile { it != '\n' } + .trim() + val markerEndOffset = markerOffset + caretMarker.length - 1 + val markerLine = doc.getLineNumber(markerEndOffset) + val makerColumn = markerEndOffset - doc.getLineStartOffset(markerLine) + val elementOffset = min(doc.getLineStartOffset(markerLine - 1) + makerColumn, doc.getLineEndOffset(markerLine - 1)) + val elementAtMarker = file.findElementAt(elementOffset)!! + + val element = PsiTreeUtil.getParentOfType(elementAtMarker, psiClass, false) + if (element != null) { + result.add(Triple(element, data, elementOffset)) + } else { + val injectionElement = + InjectedLanguageManager + .getInstance(file.project) + .findInjectedElementAt(file, elementOffset) + ?.let { PsiTreeUtil.getParentOfType(it, psiClass, false) } + ?: error("No ${psiClass.simpleName} at ${elementAtMarker.text}") + val injectionOffset = + (injectionElement.containingFile.virtualFile as VirtualFileWindow) + .documentWindow + .hostToInjected(elementOffset) + result.add(Triple(injectionElement, data, injectionOffset)) + } + } + return result + } +} diff --git a/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlCompletionTestBase.kt b/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlCompletionTestBase.kt new file mode 100644 index 00000000..8b771857 --- /dev/null +++ b/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlCompletionTestBase.kt @@ -0,0 +1,64 @@ +package com.github.l34130.mise.core.lang.completion + +import com.github.l34130.mise.core.MiseTomlTestBase +import com.intellij.codeInsight.lookup.LookupElement +import org.intellij.lang.annotations.Language + +internal abstract class MiseTomlCompletionTestBase : MiseTomlTestBase() { + protected fun testSingleCompletion( + @Language("TOML") before: String, + @Language("TOML") after: String, + ) { + check(hasCaretMarker(before)) { "Please, add `/*caret*/` or `` marker to\n$before" } + check(hasCaretMarker(after)) { "Please, add `/*caret*/` or `` marker to\n$after" } + checkByText(before.trimIndent(), after.trimIndent()) { executeSoloCompletion() } + } + + protected fun testCompletion( + lookupString: String, + @Language("TOML") before: String, + @Language("TOML") after: String, + completionChar: Char, + ) { + check(hasCaretMarker(before)) { "Please, add `/*caret*/` or `` marker to\n$before" } + check(hasCaretMarker(after)) { "Please, add `/*caret*/` or `` marker to\n$after" } + + checkByText(before.trimIndent(), after.trimIndent()) { + val items = myFixture.completeBasic() ?: return@checkByText + val lookupItem = items.find { it.lookupString == lookupString } + ?: error("No lookup item found: $lookupString\nitems: ${items.joinToString { it.lookupString }}") + myFixture.lookup.currentItem = lookupItem + myFixture.type(completionChar) + } + } + + protected fun checkByText( + code: String, + after: String, + action: () -> Unit, + ) { + InlineFile(code) + action() + myFixture.checkResult(replaceCaretMarker(after)) + } + + private fun executeSoloCompletion() { + val lookups = myFixture.completeBasic() + + if (lookups != null) { + if (lookups.size == 1) { + // for cases like `frob/*caret*/nicate()`, + // completion won't be selected automatically. + myFixture.type('\n') + return + } + fun LookupElement.debug(): String = "$lookupString ($psiElement)" + error("Expected a single completion, but got ${lookups.size}\n" + + lookups.joinToString("\n") { it.debug() }) + } + } + + private fun replaceCaretMarker(text: String): String = text.replace("/*caret*/", "") + + private fun hasCaretMarker(text: String): Boolean = "" in text || "/*caret*/" in text +} diff --git a/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlTaskDependsCompletionProviderTest.kt b/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlTaskDependsCompletionProviderTest.kt new file mode 100644 index 00000000..ee3642c3 --- /dev/null +++ b/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/completion/MiseTomlTaskDependsCompletionProviderTest.kt @@ -0,0 +1,27 @@ +package com.github.l34130.mise.core.lang.completion + +internal class MiseTomlTaskDependsCompletionProviderTest : MiseTomlCompletionTestBase() { + fun `test completion inTaskDependsArray`() = testSingleCompletion(""" + [tasks.foo] + + [tasks.bar] + depends = [""] + """, """ + [tasks.foo] + + [tasks.bar] + depends = ["foo"] + """) + + fun `test completion inTaskDependsPostArray`() = testSingleCompletion(""" + [tasks.foo] + + [tasks.bar] + depends_post = [""] + """, """ + [tasks.foo] + + [tasks.bar] + depends_post = ["foo"] + """) +} diff --git a/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiPatternsTest.kt b/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiPatternsTest.kt new file mode 100644 index 00000000..9f5f403e --- /dev/null +++ b/modules/core/src/test/kotlin/com/github/l34130/mise/core/lang/psi/MiseTomlPsiPatternsTest.kt @@ -0,0 +1,51 @@ +package com.github.l34130.mise.core.lang.psi + +import com.github.l34130.mise.core.MiseTomlTestBase +import com.github.l34130.mise.core.lang.psi.MiseTomlPsiPatterns.inTaskDependsArray +import com.github.l34130.mise.core.lang.psi.MiseTomlPsiPatterns.inTaskDependsString +import com.intellij.patterns.ElementPattern +import com.intellij.psi.PsiElement +import com.jetbrains.rd.util.assert +import org.intellij.lang.annotations.Language + +class MiseTomlPsiPatternsTest : MiseTomlTestBase() { + fun `test inTaskDependsArray`() = testPattern(inTaskDependsArray, """ + [tasks.foo] + depends = ["bar", ""] + #^ + """) + + fun `test inTaskDependsArray with empty array`() = testPattern(inTaskDependsArray, """ + [tasks.foo] + depends = [""] + #^ + """) + + fun `test inTaskDependsString with`() = testPattern(inTaskDependsString, """ + [tasks.foo] + depends = "f" + #^ + """) + + fun `test inTaskDependsString with empty string`() = testPattern(inTaskDependsString, """ + [tasks.foo] + depends = "" + #^ + """) + + private inline fun testPattern( + pattern: ElementPattern, + @Language("TOML") code: String, + fileName: String = "mise.toml", + ) { + InlineFile(code, fileName) + val element = findElementInEditor() + assert(pattern.accepts(element)) { + """ + Pattern does not accept element at caret: + pattern: $pattern + $code + """.trimIndent() + } + } +} diff --git a/modules/core/src/test/resources/META-INF/plugin.xml b/modules/core/src/test/resources/META-INF/plugin.xml new file mode 100644 index 00000000..dda38dab --- /dev/null +++ b/modules/core/src/test/resources/META-INF/plugin.xml @@ -0,0 +1,21 @@ + + com.github.l34130.mise + + + + + + + + diff --git a/modules/products/goland/build.gradle.kts b/modules/products/goland/build.gradle.kts index ad6ea7d1..bdfe1ce7 100644 --- a/modules/products/goland/build.gradle.kts +++ b/modules/products/goland/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import org.jetbrains.intellij.platform.gradle.TestFrameworkType fun properties(key: String) = project.findProperty(key).toString() @@ -9,6 +10,7 @@ plugins { dependencies { implementation(project(":mise-core")) + testImplementation(libs.junit) // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin intellijPlatform { @@ -18,6 +20,6 @@ dependencies { jetbrainsRuntime() - instrumentationTools() + testFramework(TestFrameworkType.Platform) } } diff --git a/modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/go/ProjectGoSdkSetup.kt b/modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/go/MiseProjectGoSdkSetup.kt similarity index 95% rename from modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/go/ProjectGoSdkSetup.kt rename to modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/go/MiseProjectGoSdkSetup.kt index f3425070..992e463f 100644 --- a/modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/go/ProjectGoSdkSetup.kt +++ b/modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/go/MiseProjectGoSdkSetup.kt @@ -13,7 +13,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.LocalFileSystem import kotlin.reflect.KClass -class ProjectGoSdkSetup : AbstractProjectSdkSetup() { +class MiseProjectGoSdkSetup : AbstractProjectSdkSetup() { override fun getDevToolName() = MiseDevToolName("go") override fun setupSdk( diff --git a/modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/run/GoLandRunConfigurationExtension.kt b/modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/run/MiseGoLandRunConfigurationExtension.kt similarity index 56% rename from modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/run/GoLandRunConfigurationExtension.kt rename to modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/run/MiseGoLandRunConfigurationExtension.kt index 4385d914..c253de62 100644 --- a/modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/run/GoLandRunConfigurationExtension.kt +++ b/modules/products/goland/src/main/kotlin/com/github/l34130/mise/goland/run/MiseGoLandRunConfigurationExtension.kt @@ -1,20 +1,16 @@ package com.github.l34130.mise.goland.run -import com.github.l34130.mise.core.command.MiseCommandLineHelper -import com.github.l34130.mise.core.command.MiseCommandLineNotFoundException -import com.github.l34130.mise.core.notification.MiseNotificationServiceUtils +import com.github.l34130.mise.core.MiseHelper import com.github.l34130.mise.core.run.MiseRunConfigurationSettingsEditor -import com.github.l34130.mise.core.setting.MiseSettings import com.goide.execution.GoRunConfigurationBase import com.goide.execution.GoRunningState import com.goide.execution.extension.GoRunConfigurationExtension import com.intellij.execution.configurations.RunnerSettings import com.intellij.execution.target.TargetedCommandLineBuilder -import com.intellij.openapi.components.service import com.intellij.openapi.options.SettingsEditor import org.jdom.Element -class GoLandRunConfigurationExtension : GoRunConfigurationExtension() { +class MiseGoLandRunConfigurationExtension : GoRunConfigurationExtension() { override fun getEditorTitle(): String = MiseRunConfigurationSettingsEditor.EDITOR_TITLE override fun

> createEditor(configuration: P): SettingsEditor

= @@ -44,26 +40,9 @@ class GoLandRunConfigurationExtension : GoRunConfigurationExtension() { state: GoRunningState>, commandLineType: GoRunningState.CommandLineType, ) { - val project = configuration.getProject() - val projectState = project.service().state - val runConfigState = MiseRunConfigurationSettingsEditor.getMiseRunConfigurationState(configuration) - - val (workDir, configEnvironment) = when { - projectState.useMiseDirEnv -> project.basePath to projectState.miseConfigEnvironment - runConfigState?.useMiseDirEnv == true -> configuration.getWorkingDirectory() to runConfigState.miseConfigEnvironment - else -> return - } - - MiseCommandLineHelper.getEnvVars(workDir, configEnvironment) - .fold( - onSuccess = { envVars -> envVars }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) - } - emptyMap() - }, - ).forEach { (k, v) -> cmdLine.addEnvironmentVariable(k, v) } + MiseHelper + .getMiseEnvVarsOrNotify(configuration, configuration::getWorkingDirectory) + .forEach { (k, v) -> cmdLine.addEnvironmentVariable(k, v) } } override fun isApplicableFor(configuration: GoRunConfigurationBase<*>): Boolean = true diff --git a/modules/products/goland/src/test/kotlin/com/github/l34130/mise/goland/GoLandTest.kt b/modules/products/goland/src/test/kotlin/com/github/l34130/mise/goland/GoLandTest.kt new file mode 100644 index 00000000..c2f27dd4 --- /dev/null +++ b/modules/products/goland/src/test/kotlin/com/github/l34130/mise/goland/GoLandTest.kt @@ -0,0 +1,9 @@ +package com.github.l34130.mise.goland + +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class GoLandTest : BasePlatformTestCase() { + fun testGoLand() { + println("GoLandTest.testGoLand") + } +} diff --git a/modules/products/gradle/build.gradle.kts b/modules/products/gradle/build.gradle.kts index cad68150..59832245 100644 --- a/modules/products/gradle/build.gradle.kts +++ b/modules/products/gradle/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import org.jetbrains.intellij.platform.gradle.TestFrameworkType fun properties(key: String) = project.findProperty(key).toString() @@ -10,6 +11,7 @@ plugins { dependencies { implementation(project(":mise-core")) + testImplementation(libs.junit) // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin intellijPlatform { @@ -19,6 +21,6 @@ dependencies { jetbrainsRuntime() - instrumentationTools() + testFramework(TestFrameworkType.Platform) } } diff --git a/modules/products/gradle/src/main/kotlin/com/github/l34130/mise/gradle/run/GradleEnvironmentProvider.kt b/modules/products/gradle/src/main/kotlin/com/github/l34130/mise/gradle/run/GradleEnvironmentProvider.kt deleted file mode 100644 index def4d5a4..00000000 --- a/modules/products/gradle/src/main/kotlin/com/github/l34130/mise/gradle/run/GradleEnvironmentProvider.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.github.l34130.mise.gradle.run - -import com.github.l34130.mise.core.command.MiseCommandLineHelper -import com.github.l34130.mise.core.command.MiseCommandLineNotFoundException -import com.github.l34130.mise.core.notification.MiseNotificationServiceUtils -import com.github.l34130.mise.core.run.MiseRunConfigurationSettingsEditor -import com.github.l34130.mise.core.setting.MiseSettings -import com.intellij.execution.Executor -import com.intellij.execution.application.ApplicationConfiguration -import com.intellij.execution.runners.ExecutionEnvironment -import com.intellij.openapi.components.service -import com.intellij.openapi.project.Project -import com.intellij.task.ExecuteRunConfigurationTask -import org.jetbrains.plugins.gradle.execution.build.GradleExecutionEnvironmentProvider -import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration - -class GradleEnvironmentProvider : GradleExecutionEnvironmentProvider { - override fun isApplicable(task: ExecuteRunConfigurationTask?): Boolean = task?.runProfile is ApplicationConfiguration - - override fun createExecutionEnvironment( - project: Project, - task: ExecuteRunConfigurationTask, - executor: Executor, - ): ExecutionEnvironment? { - val environment = - GradleExecutionEnvironmentProvider.EP_NAME.extensions - .firstOrNull { provider -> - provider != this && provider.isApplicable(task) - }?.createExecutionEnvironment(project, task, executor) - - if (environment?.runProfile !is GradleRunConfiguration) { - return environment - } - val gradleRunConfiguration = environment.runProfile as GradleRunConfiguration - - val projectState = project.service().state - val runConfigState = MiseRunConfigurationSettingsEditor.getMiseRunConfigurationState(gradleRunConfiguration) - - val (workDir, configEnvironment) = when { - projectState.useMiseDirEnv -> project.basePath to projectState.miseConfigEnvironment - runConfigState?.useMiseDirEnv == true -> { - val sourceConfig = task.runProfile as ApplicationConfiguration - sourceConfig.project.basePath to runConfigState.miseConfigEnvironment - } - - else -> return environment - } - - val miseEnvVars = MiseCommandLineHelper.getEnvVars(workDir, configEnvironment) - .fold( - onSuccess = { envVars -> envVars }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) - } - emptyMap() - }, - ) - - gradleRunConfiguration.settings.env = miseEnvVars + gradleRunConfiguration.settings.env - return environment - } -} diff --git a/modules/products/gradle/src/main/kotlin/com/github/l34130/mise/gradle/run/MiseGradleEnvironmentProvider.kt b/modules/products/gradle/src/main/kotlin/com/github/l34130/mise/gradle/run/MiseGradleEnvironmentProvider.kt new file mode 100644 index 00000000..630d7cec --- /dev/null +++ b/modules/products/gradle/src/main/kotlin/com/github/l34130/mise/gradle/run/MiseGradleEnvironmentProvider.kt @@ -0,0 +1,64 @@ +package com.github.l34130.mise.gradle.run + +import com.github.l34130.mise.core.MiseHelper +import com.intellij.execution.Executor +import com.intellij.execution.runners.ExecutionEnvironment +import com.intellij.execution.runners.ProgramRunner +import com.intellij.openapi.externalSystem.util.ExternalSystemUtil +import com.intellij.openapi.project.Project +import com.intellij.task.ExecuteRunConfigurationTask +import org.jetbrains.plugins.gradle.execution.build.GradleExecutionEnvironmentProvider +import org.jetbrains.plugins.gradle.service.execution.GradleRunConfiguration +import org.jetbrains.plugins.gradle.util.GradleConstants + +class MiseGradleEnvironmentProvider : GradleExecutionEnvironmentProvider { + override fun isApplicable(task: ExecuteRunConfigurationTask?): Boolean = task?.runProfile is GradleRunConfiguration + + override fun createExecutionEnvironment( + project: Project, + task: ExecuteRunConfigurationTask, + executor: Executor, + ): ExecutionEnvironment? { + val runProfile = task.runProfile as GradleRunConfiguration + var environment: ExecutionEnvironment? = delegateProvider(task)?.createExecutionEnvironment(project, task, executor) + + // No registered environment provider. Gradle Task is in this case. + if (environment == null) { + environment = + ExternalSystemUtil.createExecutionEnvironment( + runProfile.project, + GradleConstants.SYSTEM_ID, + runProfile.settings.clone(), + executor.id, + ) + } + + // Code Coverage runs maybe in this case. + if (environment == null) { + val runner = ProgramRunner.getRunner(executor.id, task.runProfile) + val taskSettings = task.settings + if (runner != null && taskSettings != null) { + environment = + ExecutionEnvironment(executor, runner, taskSettings, project) + } + } + + if (environment?.runProfile is GradleRunConfiguration) { + val miseEnvVars = + MiseHelper.getMiseEnvVarsOrNotify( + configuration = runProfile, + workingDirectory = { runProfile.projectPathOnTarget }, + ) + + val settings = (environment.runProfile as GradleRunConfiguration).settings + settings.env = settings.env + miseEnvVars + } + + return environment + } + + private fun delegateProvider(task: ExecuteRunConfigurationTask): GradleExecutionEnvironmentProvider? { + val extensions = GradleExecutionEnvironmentProvider.EP_NAME.extensions + return extensions.firstOrNull { it !== this && it.isApplicable(task) } + } +} diff --git a/modules/products/gradle/src/test/kotlin/com/github/l34130/mise/gradle/GradleTest.kt b/modules/products/gradle/src/test/kotlin/com/github/l34130/mise/gradle/GradleTest.kt new file mode 100644 index 00000000..3a91f056 --- /dev/null +++ b/modules/products/gradle/src/test/kotlin/com/github/l34130/mise/gradle/GradleTest.kt @@ -0,0 +1,9 @@ +package com.github.l34130.mise.gradle + +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class GradleTest : BasePlatformTestCase() { + fun testGradle() { + println("GradleTest.testGradle") + } +} diff --git a/modules/products/idea/build.gradle.kts b/modules/products/idea/build.gradle.kts index be33ae06..7f62fd82 100644 --- a/modules/products/idea/build.gradle.kts +++ b/modules/products/idea/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import org.jetbrains.intellij.platform.gradle.TestFrameworkType fun properties(key: String) = project.findProperty(key).toString() @@ -9,6 +10,7 @@ plugins { dependencies { implementation(project(":mise-core")) + testImplementation(libs.junit) // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin intellijPlatform { @@ -18,6 +20,6 @@ dependencies { jetbrainsRuntime() - instrumentationTools() + testFramework(TestFrameworkType.Platform) } } diff --git a/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/jdk/ProjectJdkSetup.kt b/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/jdk/MiseProjectJdkSetup.kt similarity index 97% rename from modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/jdk/ProjectJdkSetup.kt rename to modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/jdk/MiseProjectJdkSetup.kt index 00c6b70b..48f7c153 100644 --- a/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/jdk/ProjectJdkSetup.kt +++ b/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/jdk/MiseProjectJdkSetup.kt @@ -10,7 +10,7 @@ import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.roots.ProjectRootManager import kotlin.reflect.KClass -class ProjectJdkSetup : AbstractProjectSdkSetup() { +class MiseProjectJdkSetup : AbstractProjectSdkSetup() { override fun getDevToolName() = MiseDevToolName("java") override fun setupSdk( diff --git a/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/run/IdeaRunConfigurationExtension.kt b/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/run/IdeaRunConfigurationExtension.kt deleted file mode 100644 index 144dcf2f..00000000 --- a/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/run/IdeaRunConfigurationExtension.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.l34130.mise.idea.run - -import com.github.l34130.mise.core.command.MiseCommandLineHelper -import com.github.l34130.mise.core.command.MiseCommandLineNotFoundException -import com.github.l34130.mise.core.notification.MiseNotificationServiceUtils -import com.github.l34130.mise.core.run.MiseRunConfigurationSettingsEditor -import com.github.l34130.mise.core.setting.MiseSettings -import com.intellij.execution.RunConfigurationExtension -import com.intellij.execution.configurations.GeneralCommandLine -import com.intellij.execution.configurations.JavaParameters -import com.intellij.execution.configurations.RunConfigurationBase -import com.intellij.execution.configurations.RunnerSettings -import com.intellij.openapi.components.service -import com.intellij.openapi.options.SettingsEditor -import org.jdom.Element - -class IdeaRunConfigurationExtension : RunConfigurationExtension() { - override fun getEditorTitle(): String = MiseRunConfigurationSettingsEditor.EDITOR_TITLE - - override fun

> createEditor(configuration: P): SettingsEditor

= - MiseRunConfigurationSettingsEditor(configuration.project) - - override fun getSerializationId(): String = MiseRunConfigurationSettingsEditor.SERIALIZATION_ID - - override fun readExternal( - runConfiguration: RunConfigurationBase<*>, - element: Element, - ) { - MiseRunConfigurationSettingsEditor.readExternal(runConfiguration, element) - } - - override fun writeExternal( - runConfiguration: RunConfigurationBase<*>, - element: Element, - ) { - MiseRunConfigurationSettingsEditor.writeExternal(runConfiguration, element) - } - - override fun > updateJavaParameters( - configuration: T, - params: JavaParameters, - runnerSettings: RunnerSettings?, - ) { - val sourceEnv = - GeneralCommandLine() - .withEnvironment(params.env) - .withParentEnvironmentType( - if (params.isPassParentEnvs) { - GeneralCommandLine.ParentEnvironmentType.CONSOLE - } else { - GeneralCommandLine.ParentEnvironmentType.NONE - }, - ).effectiveEnvironment - params.env.putAll(sourceEnv) - - val project = configuration.project - val projectState = project.service().state - val runConfigState = MiseRunConfigurationSettingsEditor.getMiseRunConfigurationState(configuration) - - val (workDir, configEnvironment) = when { - projectState.useMiseDirEnv -> project.basePath to projectState.miseConfigEnvironment - runConfigState?.useMiseDirEnv == true -> params.workingDirectory to runConfigState.miseConfigEnvironment - else -> return - } - - val envVars = MiseCommandLineHelper.getEnvVars(workDir, configEnvironment) - .fold( - onSuccess = { envVars -> envVars }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) - } - emptyMap() - }, - ) - - params.env.putAll(envVars) - } - - override fun isApplicableFor(configuration: RunConfigurationBase<*>): Boolean = true - - override fun isEnabledFor( - applicableConfiguration: RunConfigurationBase<*>, - runnerSettings: RunnerSettings?, - ): Boolean = true -} diff --git a/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/run/MiseIdeaRunConfigurationExtension.kt b/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/run/MiseIdeaRunConfigurationExtension.kt new file mode 100644 index 00000000..009b736c --- /dev/null +++ b/modules/products/idea/src/main/kotlin/com/github/l34130/mise/idea/run/MiseIdeaRunConfigurationExtension.kt @@ -0,0 +1,86 @@ +package com.github.l34130.mise.idea.run + +import com.github.l34130.mise.core.MiseHelper +import com.github.l34130.mise.core.run.MiseRunConfigurationSettingsEditor +import com.intellij.execution.RunConfigurationExtension +import com.intellij.execution.configurations.JavaParameters +import com.intellij.execution.configurations.RunConfigurationBase +import com.intellij.execution.configurations.RunnerSettings +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessHandler +import com.intellij.execution.process.ProcessListener +import com.intellij.openapi.externalSystem.service.execution.ExternalSystemRunConfiguration +import com.intellij.openapi.options.SettingsEditor +import com.intellij.openapi.project.Project +import org.jdom.Element +import java.util.concurrent.ConcurrentHashMap + +class MiseIdeaRunConfigurationExtension : RunConfigurationExtension() { + // Used for cleanup the configuration after the execution has ended. + private val runningProcessEnvs = ConcurrentHashMap>() + + override fun getEditorTitle(): String = MiseRunConfigurationSettingsEditor.EDITOR_TITLE + + override fun

> createEditor(configuration: P): SettingsEditor

= + MiseRunConfigurationSettingsEditor(configuration.project) + + override fun getSerializationId(): String = MiseRunConfigurationSettingsEditor.SERIALIZATION_ID + + override fun readExternal( + runConfiguration: RunConfigurationBase<*>, + element: Element, + ) { + MiseRunConfigurationSettingsEditor.readExternal(runConfiguration, element) + } + + override fun writeExternal( + runConfiguration: RunConfigurationBase<*>, + element: Element, + ) { + MiseRunConfigurationSettingsEditor.writeExternal(runConfiguration, element) + } + + override fun > updateJavaParameters( + configuration: T, + params: JavaParameters, + runnerSettings: RunnerSettings?, + ) { + val envVars = MiseHelper.getMiseEnvVarsOrNotify(configuration, params::getWorkingDirectory) + params.env = params.env + envVars + + // Gradle support (and external system configuration) + // When user double-clicks the Task in the Gradle tool window. + if (configuration is ExternalSystemRunConfiguration) { + runningProcessEnvs[configuration.project] = configuration.settings.env.toMap() + configuration.settings.env = configuration.settings.env + envVars + } + } + + override fun attachToProcess( + configuration: RunConfigurationBase<*>, + handler: ProcessHandler, + runnerSettings: RunnerSettings?, + ) { + if (configuration is ExternalSystemRunConfiguration) { + val envsToRestore = runningProcessEnvs.remove(configuration.project) ?: return + + handler.addProcessListener( + object : ProcessListener { + override fun processTerminated(event: ProcessEvent) { + configuration.settings.env.apply { + clear() + putAll(envsToRestore) + } + } + }, + ) + } + } + + override fun isApplicableFor(configuration: RunConfigurationBase<*>): Boolean = true + + override fun isEnabledFor( + applicableConfiguration: RunConfigurationBase<*>, + runnerSettings: RunnerSettings?, + ): Boolean = true +} diff --git a/modules/products/idea/src/test/kotlin/com/github/l34130/mise/idea/IdeaTest.kt b/modules/products/idea/src/test/kotlin/com/github/l34130/mise/idea/IdeaTest.kt new file mode 100644 index 00000000..eef3a38c --- /dev/null +++ b/modules/products/idea/src/test/kotlin/com/github/l34130/mise/idea/IdeaTest.kt @@ -0,0 +1,9 @@ +package com.github.l34130.mise.idea + +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class IdeaTest : BasePlatformTestCase() { + fun testIdea() { + println("IdeaTest.testIdea") + } +} diff --git a/modules/products/nodejs/build.gradle.kts b/modules/products/nodejs/build.gradle.kts index 3a002ff2..088ab504 100644 --- a/modules/products/nodejs/build.gradle.kts +++ b/modules/products/nodejs/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType +import org.jetbrains.intellij.platform.gradle.TestFrameworkType fun properties(key: String) = project.findProperty(key).toString() @@ -9,6 +10,7 @@ plugins { dependencies { implementation(project(":mise-core")) + testImplementation(libs.junit) // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin intellijPlatform { @@ -18,7 +20,6 @@ dependencies { plugins("deno:233.6745.297") jetbrainsRuntime() - - instrumentationTools() + testFramework(TestFrameworkType.Platform) } } diff --git a/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/deno/ProjectDenoSetup.kt b/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/deno/MiseProjectDenoSetup.kt similarity index 95% rename from modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/deno/ProjectDenoSetup.kt rename to modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/deno/MiseProjectDenoSetup.kt index efd24ce3..60577f92 100644 --- a/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/deno/ProjectDenoSetup.kt +++ b/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/deno/MiseProjectDenoSetup.kt @@ -12,7 +12,7 @@ import com.intellij.openapi.util.io.FileUtil import kotlin.io.path.Path import kotlin.reflect.KClass -class ProjectDenoSetup : AbstractProjectSdkSetup() { +class MiseProjectDenoSetup : AbstractProjectSdkSetup() { override fun getDevToolName() = MiseDevToolName("deno") override fun setupSdk( diff --git a/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/node/ProjectNodeSetup.kt b/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/node/MiseProjectNodeSetup.kt similarity index 97% rename from modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/node/ProjectNodeSetup.kt rename to modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/node/MiseProjectNodeSetup.kt index f5de5434..6a198f6a 100644 --- a/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/node/ProjectNodeSetup.kt +++ b/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/node/MiseProjectNodeSetup.kt @@ -15,7 +15,7 @@ import com.intellij.openapi.vfs.LocalFileSystem import kotlin.io.path.Path import kotlin.reflect.KClass -class ProjectNodeSetup : AbstractProjectSdkSetup() { +class MiseProjectNodeSetup : AbstractProjectSdkSetup() { override fun getDevToolName() = MiseDevToolName("node") override fun setupSdk( diff --git a/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/run/NodeRunConfigurationExtension.kt b/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/run/MiseNodeRunConfigurationExtension.kt similarity index 56% rename from modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/run/NodeRunConfigurationExtension.kt rename to modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/run/MiseNodeRunConfigurationExtension.kt index 7bae9b25..bd41b615 100644 --- a/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/run/NodeRunConfigurationExtension.kt +++ b/modules/products/nodejs/src/main/kotlin/com/github/l34130/mise/nodejs/run/MiseNodeRunConfigurationExtension.kt @@ -1,23 +1,20 @@ package com.github.l34130.mise.nodejs.run -import com.github.l34130.mise.core.command.MiseCommandLineHelper -import com.github.l34130.mise.core.command.MiseCommandLineNotFoundException -import com.github.l34130.mise.core.notification.MiseNotificationServiceUtils +import com.github.l34130.mise.core.MiseHelper import com.github.l34130.mise.core.run.MiseRunConfigurationSettingsEditor -import com.github.l34130.mise.core.setting.MiseSettings import com.intellij.execution.runners.ExecutionEnvironment import com.intellij.javascript.nodejs.execution.AbstractNodeTargetRunProfile import com.intellij.javascript.nodejs.execution.NodeTargetRun import com.intellij.javascript.nodejs.execution.runConfiguration.AbstractNodeRunConfigurationExtension import com.intellij.javascript.nodejs.execution.runConfiguration.NodeRunConfigurationLaunchSession -import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.options.SettingsEditor +import com.jetbrains.nodejs.run.NodeJsRunConfiguration import org.jdom.Element -class NodeRunConfigurationExtension : AbstractNodeRunConfigurationExtension() { +class MiseNodeRunConfigurationExtension : AbstractNodeRunConfigurationExtension() { companion object { - private val LOG = Logger.getInstance(NodeRunConfigurationExtension::class.java) + private val LOG = Logger.getInstance(MiseNodeRunConfigurationExtension::class.java) } override fun getEditorTitle(): String = MiseRunConfigurationSettingsEditor.EDITOR_TITLE @@ -45,30 +42,19 @@ class NodeRunConfigurationExtension : AbstractNodeRunConfigurationExtension() { configuration: AbstractNodeTargetRunProfile, environment: ExecutionEnvironment, ): NodeRunConfigurationLaunchSession? { - val project = configuration.project - val projectState = project.service().state - val runConfigState = MiseRunConfigurationSettingsEditor.getMiseRunConfigurationState(configuration) - - val (workDir, configEnvironment) = - when { - projectState.useMiseDirEnv -> project.basePath to projectState.miseConfigEnvironment - runConfigState?.useMiseDirEnv == true -> environment.modulePath to runConfigState.miseConfigEnvironment - else -> return null - } + val envVars = + MiseHelper.getMiseEnvVarsOrNotify( + configuration = configuration, + workingDirectory = { + when (configuration) { + is NodeJsRunConfiguration -> configuration.workingDirectory + else -> null + } + }, + ) return object : NodeRunConfigurationLaunchSession() { override fun addNodeOptionsTo(targetRun: NodeTargetRun) { - val envVars = MiseCommandLineHelper.getEnvVars(workDir, configEnvironment) - .fold( - onSuccess = { envVars -> envVars }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) - } - emptyMap() - }, - ) - for ((key, value) in envVars) { targetRun.commandLineBuilder.addEnvironmentVariable(key, value) } diff --git a/modules/products/nodejs/src/test/kotlin/com/github/l34130/mise/nodejs/NodeJsTest.kt b/modules/products/nodejs/src/test/kotlin/com/github/l34130/mise/nodejs/NodeJsTest.kt new file mode 100644 index 00000000..03418ad3 --- /dev/null +++ b/modules/products/nodejs/src/test/kotlin/com/github/l34130/mise/nodejs/NodeJsTest.kt @@ -0,0 +1,9 @@ +package com.github.l34130.mise.nodejs + +import com.intellij.testFramework.fixtures.BasePlatformTestCase + +class NodeJsTest : BasePlatformTestCase() { + fun testNodeJs() { + println("NodeJsTest.testNodeJs") + } +} diff --git a/modules/products/rider/build.gradle.kts b/modules/products/rider/build.gradle.kts index 71f6fc14..3d0e65b9 100644 --- a/modules/products/rider/build.gradle.kts +++ b/modules/products/rider/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { implementation(project(":mise-core")) +// testImplementation(libs.junit) // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin intellijPlatform { @@ -16,6 +17,6 @@ dependencies { jetbrainsRuntime() - instrumentationTools() +// testFramework(TestFrameworkType.Platform) } } diff --git a/modules/products/rider/src/main/kotlin/com/github/l34130/mise/rider/run/RiderPatchCommandLineExtension.kt b/modules/products/rider/src/main/kotlin/com/github/l34130/mise/rider/run/MiseRiderPatchCommandLineExtension.kt similarity index 65% rename from modules/products/rider/src/main/kotlin/com/github/l34130/mise/rider/run/RiderPatchCommandLineExtension.kt rename to modules/products/rider/src/main/kotlin/com/github/l34130/mise/rider/run/MiseRiderPatchCommandLineExtension.kt index 6dba50ea..6965d42b 100644 --- a/modules/products/rider/src/main/kotlin/com/github/l34130/mise/rider/run/RiderPatchCommandLineExtension.kt +++ b/modules/products/rider/src/main/kotlin/com/github/l34130/mise/rider/run/MiseRiderPatchCommandLineExtension.kt @@ -7,7 +7,9 @@ import com.github.l34130.mise.core.setting.MiseSettings import com.intellij.execution.configurations.GeneralCommandLine import com.intellij.execution.process.ProcessInfo import com.intellij.execution.process.ProcessListener +import com.intellij.openapi.components.service import com.intellij.openapi.project.Project +import com.intellij.util.application import com.jetbrains.rd.util.lifetime.Lifetime import com.jetbrains.rider.projectView.solutionDirectoryPath import com.jetbrains.rider.run.PatchCommandLineExtension @@ -16,7 +18,7 @@ import com.jetbrains.rider.runtime.DotNetRuntime import org.jetbrains.concurrency.Promise import org.jetbrains.concurrency.resolvedPromise -class RiderPatchCommandLineExtension : PatchCommandLineExtension { +class MiseRiderPatchCommandLineExtension : PatchCommandLineExtension { override fun patchDebugCommandLine( lifetime: Lifetime, workerRunInfo: WorkerRunInfo, @@ -40,23 +42,25 @@ class RiderPatchCommandLineExtension : PatchCommandLineExtension { commandLine: GeneralCommandLine, project: Project, ) { - val projectState = MiseSettings.getService(project).state + val projectState = application.service().state if (!projectState.useMiseDirEnv) { return } - val miseEnvVars = MiseCommandLineHelper.getEnvVars( - workDir = project.solutionDirectoryPath.toAbsolutePath().toString(), - configEnvironment = projectState.miseConfigEnvironment - ).fold( - onSuccess = { envVars -> envVars }, - onFailure = { - if (it !is MiseCommandLineNotFoundException) { - MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) - } - emptyMap() - }, - ) + val miseEnvVars = + MiseCommandLineHelper + .getEnvVars( + workDir = project.solutionDirectoryPath.toAbsolutePath().toString(), + configEnvironment = projectState.miseConfigEnvironment, + ).fold( + onSuccess = { envVars -> envVars }, + onFailure = { + if (it !is MiseCommandLineNotFoundException) { + MiseNotificationServiceUtils.notifyException("Failed to load environment variables", it) + } + emptyMap() + }, + ) commandLine.withEnvironment(miseEnvVars) } diff --git a/modules/products/toml/build.gradle.kts b/modules/products/toml/build.gradle.kts deleted file mode 100644 index 0abbd51e..00000000 --- a/modules/products/toml/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType - -fun properties(key: String) = project.findProperty(key).toString() - -plugins { - id("org.jetbrains.intellij.platform.module") - alias(libs.plugins.kotlin) // Kotlin support -} - -dependencies { - implementation(project(":mise-core")) - - // Configure Gradle IntelliJ Plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin - intellijPlatform { - create(IntelliJPlatformType.IntellijIdeaCommunity, properties("platformVersion")) - - bundledPlugin("org.toml.lang") - - jetbrainsRuntime() - - instrumentationTools() - } -} diff --git a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigCompletionContributor.kt b/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigCompletionContributor.kt deleted file mode 100644 index 7ca3f4f2..00000000 --- a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigCompletionContributor.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.l34130.mise.toml.completion - -import com.intellij.codeInsight.completion.CompletionContributor -import com.intellij.codeInsight.completion.CompletionType -import com.intellij.patterns.PlatformPatterns - -class MiseConfigCompletionContributor : CompletionContributor() { - init { - extend(CompletionType.BASIC, PlatformPatterns.psiElement(), MiseConfigCompletionProvider()) - } -} diff --git a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigCompletionProvider.kt b/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigCompletionProvider.kt deleted file mode 100644 index aa7c3300..00000000 --- a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigCompletionProvider.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.github.l34130.mise.toml.completion - -import com.github.l34130.mise.core.command.MiseCommandLineHelper -import com.github.l34130.mise.core.command.MiseTask -import com.github.l34130.mise.core.icon.MiseIcons -import com.github.l34130.mise.core.setting.MiseSettings -import com.intellij.codeInsight.completion.CompletionParameters -import com.intellij.codeInsight.completion.CompletionProvider -import com.intellij.codeInsight.completion.CompletionResultSet -import com.intellij.codeInsight.lookup.LookupElementBuilder -import com.intellij.openapi.util.io.FileUtil -import com.intellij.util.ProcessingContext -import kotlin.io.path.Path - -fun normalizePath(path: String) = - Path( - FileUtil.expandUserHome(path), - ).normalize().toAbsolutePath().toString() - -class MiseConfigCompletionProvider : CompletionProvider() { - override fun addCompletions( - params: CompletionParameters, - context: ProcessingContext, - resultSet: CompletionResultSet, - ) { - val position = params.position - val root = position.containingFile - - if (!MiseConfigPsiUtils.isInTaskDepends(position)) { - return - } - - val configEnvironment = MiseSettings.getService(root.project).state.miseConfigEnvironment - // TODO: Store the tasks in a cache and update them only when the file changes - // This will prevent unnecessary calls to the CLI or when the file is an invalid state - val tasksFromMise = MiseCommandLineHelper.getTasks( - workDir = root.project.basePath, - configEnvironment = configEnvironment, - ).fold( - onSuccess = { tasks -> tasks }, - onFailure = { emptyList() }, - ) - - val tasksInFile = MiseConfigPsiUtils.getMiseTasks(root) - val uniqueTasks = HashMap() - val currentPath = - normalizePath( - Path(root.project.basePath ?: "", root.viewProvider.virtualFile.path).toString(), - ) - - tasksFromMise - .filter { - when (it.source) { - null -> true - else -> normalizePath(it.source!!) != currentPath - } - }.forEach { uniqueTasks[it.name] = it } - tasksInFile.forEach { uniqueTasks[it.name] = it } - - resultSet.addAllElements( - uniqueTasks - .filter { it.key.isNotBlank() } - .map { - LookupElementBuilder - .create(it.key) - .withIcon(MiseIcons.DEFAULT) - .withTypeText(it.value.description ?: it.value.command) - .withCaseSensitivity(false) - }, - ) - } -} diff --git a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigPsiUtils.kt b/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigPsiUtils.kt deleted file mode 100644 index d5b0c2c7..00000000 --- a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/completion/MiseConfigPsiUtils.kt +++ /dev/null @@ -1,146 +0,0 @@ -package com.github.l34130.mise.toml.completion - -import com.github.l34130.mise.core.command.MiseTask -import com.intellij.psi.ElementManipulators -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import org.toml.lang.psi.TomlArray -import org.toml.lang.psi.TomlKeyValue -import org.toml.lang.psi.TomlLiteral -import org.toml.lang.psi.TomlTable - -object MiseConfigPsiUtils { - fun getMiseTasks(psiFile: PsiFile): List { - val tasks = mutableListOf() - - // Handle table-style tasks ([tasks.abc]) - tasks.addAll(psiFile.children.filterIsInstance().mapNotNull { it.parseTableStyleTask() }) - - // Handle key-value style tasks (a = 'echo a') - psiFile.children - .filterIsInstance() - .find { it.header.key?.textMatches("tasks") == true } - ?.let { tasksTable -> - tasks.addAll( - tasksTable.entries - .mapNotNull { it.parseKeyValueStyleTask() }, - ) - } - - return tasks - } - - fun isInTaskDepends(psiElement: PsiElement): Boolean { - val tomlArray = psiElement.parent.parent as? TomlArray ?: return false - val tomlKeyValue = tomlArray.parent as? TomlKeyValue ?: return false - - // Check if we're in a table-style task - val tomlTable = tomlKeyValue.parent as? TomlTable - if (tomlTable != null && tomlTable.isMiseTask()) { - return tomlKeyValue.key.textMatches("depends") - } - - // Check if we're in a key-value style task's depends - val tasksTable = tomlKeyValue.parent as? TomlTable - return tasksTable?.header?.key?.textMatches("tasks") == true && - tomlKeyValue.key.textMatches("depends") - } -} - -fun TomlTable.isMiseTask(): Boolean { - val headerKey = header.key ?: return false - val firstSegment = headerKey.segments.firstOrNull() ?: return false - return ElementManipulators.getValueText(firstSegment) == "tasks" -} - -fun TomlTable.parseTableStyleTask(): MiseTask? { - if (!isMiseTask()) { - return null - } - - val headerKey = header.key!! - val segments = headerKey.segments.drop(1) // Skip the initial "tasks" segment - val taskName = segments.joinToString(".") { ElementManipulators.getValueText(it) } - - return parseTaskProperties(entries)?.copy(name = taskName) -} - -fun TomlKeyValue.parseKeyValueStyleTask(): MiseTask? { - val taskName = ElementManipulators.getValueText(key) - if (value is TomlLiteral) { - return MiseTask( - name = taskName, - aliases = null, - depends = null, - description = null, - hide = false, - command = ElementManipulators.getValueText(value as TomlLiteral), - ) - } - - // Handle table-style task definition - val tableValue = value as? TomlTable ?: return null - return parseTaskProperties(tableValue.entries)?.copy(name = taskName) -} - -private fun parseTaskProperties(entries: List): MiseTask? { - var aliases: List? = null - var depends: List? = null - var description: String? = null - var hide = false - var command: String? = null - - for (entry in entries) { - val key = entry.key - val value = entry.value - when { - key.textMatches("alias") -> { - when (value) { - is TomlLiteral -> aliases = listOf(ElementManipulators.getValueText(value)) - is TomlArray -> - aliases = - value.elements - .mapNotNull { it as? TomlLiteral } - .map { ElementManipulators.getValueText(it) } - } - } - - key.textMatches("depends") -> { - when (value) { - is TomlArray -> - depends = - value.elements - .mapNotNull { it as? TomlLiteral } - .map { ElementManipulators.getValueText(it) } - } - } - - key.textMatches("description") -> { - if (value is TomlLiteral) { - description = ElementManipulators.getValueText(value) - } - } - - key.textMatches("hide") -> { - if (value is TomlLiteral) { - hide = ElementManipulators.getValueText(value).toBoolean() - } - } - - key.textMatches("run") -> { - if (value is TomlLiteral) { - command = ElementManipulators.getValueText(value) - } - } - } - } - - return MiseTask( - name = "", // set by caller - aliases = aliases, - depends = depends, - description = description, - hide = hide, - command = command, - ) -} diff --git a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/editor/MiseRunLineMarkerProvider.kt b/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/editor/MiseRunLineMarkerProvider.kt deleted file mode 100644 index 7e535efc..00000000 --- a/modules/products/toml/src/main/kotlin/com/github/l34130/mise/toml/editor/MiseRunLineMarkerProvider.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.github.l34130.mise.toml.editor - -import com.github.l34130.mise.core.command.MiseRunTaskOnTerminalAction -import com.github.l34130.mise.core.setting.MiseSettings -import com.intellij.execution.lineMarker.RunLineMarkerContributor -import com.intellij.openapi.components.service -import com.intellij.psi.PsiElement -import com.intellij.psi.impl.source.tree.LeafPsiElement -import org.toml.lang.psi.TomlKey -import org.toml.lang.psi.TomlKeySegment -import org.toml.lang.psi.TomlKeyValue -import org.toml.lang.psi.TomlTable -import org.toml.lang.psi.TomlTableHeader - -class MiseRunLineMarkerProvider : RunLineMarkerContributor() { - override fun getInfo(element: PsiElement): Info? { - val isMiseFile = element.containingFile.name.contains("mise") && element.containingFile.name.endsWith(".toml") - if (!isMiseFile || element !is LeafPsiElement) { - return null - } - - val taskName = getTaskInfo(element) ?: return null - - val miseSettings = element.project.service() - val configEnvironment = miseSettings.state.miseConfigEnvironment - - return Info(MiseRunTaskOnTerminalAction(taskName, configEnvironment)) - } - - private fun getTaskInfo(element: LeafPsiElement): String? { - val parent = element.parent ?: return null - - if (parent is TomlKeySegment) { - // Case 1: [tasks] section - // [tasks] - // taskname = "..." - // other = "..." - val keyValue = parent.parent?.parent - if (keyValue is TomlKeyValue) { - val table = findParentTable(keyValue) ?: return null - if (table.header.key?.text == "tasks") { - return parent.text - } - } - - // Case 2: Table-style task [tasks.taskname] - // [tasks.taskname] - // run = "..." - // [tasks.other] - // run = "..." - if (parent.parent is TomlKey && parent.parent.parent is TomlTableHeader && element.text != "tasks") { - val tomlKey = parent.parent as TomlKey - val segments = tomlKey.segments - if (segments.size == 2) { - if (segments[0].text == "tasks") { - return segments[1].text - } - } - } - } - - return null - } - - private fun findParentTable(element: PsiElement): TomlTable? { - var current: PsiElement? = element - while (current != null) { - if (current is TomlTable) { - return current - } - current = current.parent - } - return null - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 5e8f1333..f2bb9c69 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.intellij.platform.gradle.extensions.intellijPlatform plugins { - id("org.jetbrains.intellij.platform.settings") version "2.2.0" + id("org.jetbrains.intellij.platform.settings") version "2.2.1" } dependencyResolutionManagement { @@ -23,7 +23,6 @@ include( "modules/products/idea", "modules/products/nodejs", "modules/products/rider", - "modules/products/toml", ) rootProject.name = "mise" diff --git a/src/main/resources/META-INF/mise-deno.xml b/src/main/resources/META-INF/mise-deno.xml index da399596..0327ffc7 100644 --- a/src/main/resources/META-INF/mise-deno.xml +++ b/src/main/resources/META-INF/mise-deno.xml @@ -1,11 +1,11 @@ - + - diff --git a/src/main/resources/META-INF/mise-goland.xml b/src/main/resources/META-INF/mise-goland.xml index 6e82dce1..80994551 100644 --- a/src/main/resources/META-INF/mise-goland.xml +++ b/src/main/resources/META-INF/mise-goland.xml @@ -1,15 +1,15 @@ - + - - diff --git a/src/main/resources/META-INF/mise-gradle.xml b/src/main/resources/META-INF/mise-gradle.xml index 99af9807..0a4d6aae 100644 --- a/src/main/resources/META-INF/mise-gradle.xml +++ b/src/main/resources/META-INF/mise-gradle.xml @@ -1,6 +1,6 @@ - diff --git a/src/main/resources/META-INF/mise-idea.xml b/src/main/resources/META-INF/mise-idea.xml index 3219269a..e143aec2 100644 --- a/src/main/resources/META-INF/mise-idea.xml +++ b/src/main/resources/META-INF/mise-idea.xml @@ -1,13 +1,13 @@ - - + - diff --git a/src/main/resources/META-INF/mise-nodejs.xml b/src/main/resources/META-INF/mise-nodejs.xml index aa0b488b..c61fcf3d 100644 --- a/src/main/resources/META-INF/mise-nodejs.xml +++ b/src/main/resources/META-INF/mise-nodejs.xml @@ -1,15 +1,15 @@ - + - diff --git a/src/main/resources/META-INF/mise-rider.xml b/src/main/resources/META-INF/mise-rider.xml index dd581cd5..dd4f7f2e 100644 --- a/src/main/resources/META-INF/mise-rider.xml +++ b/src/main/resources/META-INF/mise-rider.xml @@ -1,5 +1,5 @@ - + diff --git a/src/main/resources/META-INF/mise-toml.xml b/src/main/resources/META-INF/mise-toml.xml deleted file mode 100644 index fa0e678d..00000000 --- a/src/main/resources/META-INF/mise-toml.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 15d810c9..f6b12128 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -5,6 +5,44 @@ 134130 com.intellij.modules.platform + org.toml.lang + + + + + + + + + + + + + + + + + + + + + + com.intellij.java JavaScript com.intellij.modules.rider - org.toml.lang