diff --git a/components/ide/jetbrains/backend-plugin/gradle.properties b/components/ide/jetbrains/backend-plugin/gradle.properties index 2cbce2c7b77a77..846a70cb13f697 100644 --- a/components/ide/jetbrains/backend-plugin/gradle.properties +++ b/components/ide/jetbrains/backend-plugin/gradle.properties @@ -10,7 +10,7 @@ platformType=IU platformDownloadSources=true # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 -platformPlugins=Git4Idea, org.jetbrains.plugins.terminal, com.jetbrains.codeWithMe +platformPlugins=Git4Idea, org.jetbrains.plugins.terminal, com.jetbrains.codeWithMe, org.jetbrains.plugins.yaml # Opt-out flag for bundling Kotlin standard library. # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. kotlin.stdlib.default.dependency=false diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodProjectManager.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodProjectManager.kt index bff55823fdbc3f..6e7b6f71320bb2 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodProjectManager.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodProjectManager.kt @@ -5,19 +5,31 @@ package io.gitpod.jetbrains.remote import com.intellij.ProjectTopics +import com.intellij.analysis.AnalysisScope +import com.intellij.codeInspection.actions.RunInspectionIntention +import com.intellij.codeInspection.ex.InspectionManagerEx import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.module.Module import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.ModuleListener import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.ProjectJdkTable import com.intellij.openapi.projectRoots.Sdk import com.intellij.openapi.roots.ModuleRootModificationUtil import com.intellij.openapi.roots.ProjectRootManager +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.profile.codeInspection.InspectionProfileManager +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiManager import com.intellij.util.application +import io.gitpod.jetbrains.remote.inspections.GitpodConfigInspection +import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlFile import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.future.await import kotlinx.coroutines.launch +import org.jetbrains.yaml.psi.YAMLFile +import java.nio.file.Paths import java.util.concurrent.CompletableFuture @@ -29,6 +41,34 @@ class GitpodProjectManager( configureSdks() } + init { + application.invokeLater { + try { + runInspection() + } catch (ex: Exception) { + thisLogger().error("Failed to run inspection", ex) + } + } + } + + private fun runInspection() { + val psiFile = getGitpodYamlPsiFile(project) ?: return + val profile = InspectionProfileManager.getInstance(project).currentProfile + val inspectionName = GitpodConfigInspection::class.java.simpleName + val tool = profile.getInspectionTool(inspectionName, psiFile) ?: return + val manager = InspectionManagerEx.getInstance(project) as InspectionManagerEx + val scope = AnalysisScope(psiFile) + DumbService.getInstance(project).smartInvokeLater { + RunInspectionIntention.rerunInspection(tool, manager, scope, psiFile) + } + } + + private fun getGitpodYamlPsiFile(project: Project): PsiFile? { + val basePath = project.basePath ?: return null + val vfile = VfsUtil.findFile(Paths.get(basePath, gitpodYamlFile), true) ?: return null + return PsiManager.getInstance(project).findFile(vfile) as? YAMLFile ?: return null + } + /** * It is a workaround for https://youtrack.jetbrains.com/issue/GTW-88 */ diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/inspections/GitpodConfigInspection.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/inspections/GitpodConfigInspection.kt new file mode 100644 index 00000000000000..a49197a5683162 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/inspections/GitpodConfigInspection.kt @@ -0,0 +1,65 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.inspections + +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.diagnostic.VMOptions +import com.intellij.openapi.util.BuildNumber +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import io.gitpod.jetbrains.remote.quickfixes.AddVMOptionsQuickFix +import io.gitpod.jetbrains.remote.quickfixes.ReplaceVMOptionsQuickFix +import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey +import io.gitpod.jetbrains.remote.utils.GitpodConfig.defaultXmxMiB +import io.gitpod.jetbrains.remote.utils.GitpodConfig.getJetBrainsProductName +import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlFile +import org.jetbrains.yaml.YAMLUtil +import org.jetbrains.yaml.psi.YAMLFile +import org.jetbrains.yaml.psi.YAMLKeyValue + +class GitpodConfigInspection : LocalInspectionTool() { + + private val runtimeXmxMiB = Runtime.getRuntime().maxMemory().shr(20) + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : PsiElementVisitor() { + override fun visitFile(file: PsiFile) { + if (file.name != gitpodYamlFile || file !is YAMLFile) return + val productCode = BuildNumber.currentVersion().productCode + val productName = getJetBrainsProductName(productCode) ?: return + val keyValue = YAMLUtil.getQualifiedKeyInFile(file, YamlKey.jetbrains, productName, YamlKey.vmOptions) + if (keyValue == null) { + val description = "IDE's max heap size (-Xmx) is ${runtimeXmxMiB}m, but not configured in $gitpodYamlFile" + val quickFix = AddVMOptionsQuickFix(productName, runtimeXmxMiB) + holder.registerProblem(file, description, quickFix) + return + } + val configuredXmxMiB = getUserConfiguredXmxValue(keyValue) + val quickFix = ReplaceVMOptionsQuickFix(runtimeXmxMiB) + if (configuredXmxMiB == null && runtimeXmxMiB != defaultXmxMiB) { + val description = "IDE's max heap size (-Xmx) is ${runtimeXmxMiB}m, but not configured in $gitpodYamlFile" + holder.registerProblem(keyValue, description, quickFix) + } else if (configuredXmxMiB != null && runtimeXmxMiB != configuredXmxMiB) { + val description = "IDE's max heap size (-Xmx) is ${runtimeXmxMiB}m, but -Xmx${configuredXmxMiB}m configured in $gitpodYamlFile" + holder.registerProblem(keyValue, description, quickFix) + } + } + } + } + + private fun getUserConfiguredXmxValue(vmOptionsKeyValue: YAMLKeyValue): Long? { + val vmOptions = vmOptionsKeyValue.valueText.trim().split("\\s".toRegex()) + // the rightmost option is the one to take effect + val finalXmx = vmOptions.lastOrNull { it.startsWith("-Xmx") } ?: return null + val xmxValue = finalXmx.substringAfter("-Xmx") + return try { + VMOptions.parseMemoryOption(xmxValue).shr(20) + } catch (e: IllegalArgumentException) { + // ignore invalid user configuration + null + } + } +} \ No newline at end of file diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/AddVMOptionsQuickFix.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/AddVMOptionsQuickFix.kt new file mode 100644 index 00000000000000..7a5e290ec285b6 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/AddVMOptionsQuickFix.kt @@ -0,0 +1,64 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.quickfixes + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.util.IncorrectOperationException +import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey +import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlFile +import org.jetbrains.yaml.YAMLElementGenerator +import org.jetbrains.yaml.psi.YAMLFile +import org.jetbrains.yaml.psi.YAMLKeyValue + +class AddVMOptionsQuickFix(private val productName: String, private val xmxValueMiB: Long) : LocalQuickFix { + + override fun getName() = "Add -Xmx${xmxValueMiB}m to $gitpodYamlFile" + + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val psiFile = descriptor.psiElement as? YAMLFile ?: return + val document = psiFile.viewProvider.document ?: return + val generator = YAMLElementGenerator.getInstance(project) + val jetbrainsKeyValue = findOrCreateYamlKeyValue(psiFile, YamlKey.jetbrains, "", generator) ?: return + val productKeyValue = findOrCreateYamlKeyValue(jetbrainsKeyValue, productName, "", generator) ?: return + findOrCreateYamlKeyValue(productKeyValue, YamlKey.vmOptions, "-Xmx${xmxValueMiB}m", generator) + PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document) + try { + CodeStyleManager.getInstance(project).reformat(jetbrainsKeyValue) + } catch (e: IncorrectOperationException) { + thisLogger().warn("AddVMOptionsQuickFix reformat failed", e) + } + } + + private fun findOrCreateYamlKeyValue( + parent: PsiElement, + keyText: String, + valueText: String, + generator: YAMLElementGenerator + ): PsiElement? { + var element = findElementByYamlKeyText(parent, keyText) + return if (element == null) { + element = generator.createYamlKeyValue(keyText, valueText) + parent.add(generator.createEol()) + parent.add(element) ?: return null + } else { + element + } + } + + private fun findElementByYamlKeyText(rootElement: PsiElement, keyText: String): PsiElement? { + return PsiTreeUtil.collectElements(rootElement) { + it is YAMLKeyValue && it.keyText == keyText + }.firstOrNull() + } +} \ No newline at end of file diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/ApplyVMOptionsQuickFix.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/ApplyVMOptionsQuickFix.kt new file mode 100644 index 00000000000000..63616cdfc25294 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/ApplyVMOptionsQuickFix.kt @@ -0,0 +1,23 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.quickfixes + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.diagnostic.VMOptions +import com.intellij.openapi.project.Project + +class ApplyVMOptionsQuickFix(private val quickFixName: String, private val xmxValueMiB: Long) : LocalQuickFix { + + override fun getName() = quickFixName + + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + if (VMOptions.canWriteOptions()) { + VMOptions.setOption(VMOptions.MemoryKind.HEAP, xmxValueMiB.toInt()) + } + } +} \ No newline at end of file diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/ReplaceVMOptionsQuickFix.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/ReplaceVMOptionsQuickFix.kt new file mode 100644 index 00000000000000..028c2dc2c770a1 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/quickfixes/ReplaceVMOptionsQuickFix.kt @@ -0,0 +1,36 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.quickfixes + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.project.Project +import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey +import org.jetbrains.yaml.YAMLElementGenerator +import org.jetbrains.yaml.psi.YAMLKeyValue + +class ReplaceVMOptionsQuickFix(private val xmxValueMiB: Long) : LocalQuickFix { + + override fun getName() = "Set Xmx to ${xmxValueMiB}m" + + override fun getFamilyName() = name + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + val vmOptionsKeyValue = descriptor.psiElement as? YAMLKeyValue ?: return + if (vmOptionsKeyValue.keyText != YamlKey.vmOptions) return + val vmOptions = vmOptionsKeyValue.valueText.trim().split("\\s".toRegex()) + val xmxUpdated = "-Xmx${xmxValueMiB}m" + val xmxOptions = vmOptions + .filter { it.startsWith("-Xmx") } + .map { xmxUpdated } + .ifEmpty { listOf(xmxUpdated) } + val nonXmxOptions = vmOptions + .filter { !it.startsWith("-Xmx") } + val newVmOptions = (xmxOptions + nonXmxOptions).toSortedSet().joinToString(" ") + val generator = YAMLElementGenerator.getInstance(project) + val psiElementUpdated = generator.createYamlKeyValue(YamlKey.vmOptions, newVmOptions) + vmOptionsKeyValue.replace(psiElementUpdated) + } +} \ No newline at end of file diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/GitpodConfig.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/GitpodConfig.kt new file mode 100644 index 00000000000000..faf5a287cdcb2f --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/utils/GitpodConfig.kt @@ -0,0 +1,34 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.utils + +/** + * Constants and util functions for Gitpod config spec + */ +object GitpodConfig { + + // FIXME: get from env var + const val defaultXmxMiB = 2048L + const val gitpodYamlFile = ".gitpod.yml" + + object YamlKey { + const val jetbrains = "jetbrains" + const val vmOptions = "vmoptions" + } + + /** + * map JetBrains IDE productCode to YAML key for .gitpod.yml + */ + fun getJetBrainsProductName(productCode: String): String? { + return when (productCode) { + "IC" -> "intellij" + "IU" -> "intellij" + "PS" -> "phpstorm" + "PY" -> "pycharm" + "GO" -> "goland" + else -> null + } + } +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml index bbb6f3991a9cdc..b8ad5a2666cccd 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml +++ b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml @@ -19,6 +19,7 @@ + @@ -30,6 +31,13 @@ + diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources/inspectionDescriptions/GitpodConfigInspection.html b/components/ide/jetbrains/backend-plugin/src/main/resources/inspectionDescriptions/GitpodConfigInspection.html new file mode 100644 index 00000000000000..80e06412ddbe23 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/resources/inspectionDescriptions/GitpodConfigInspection.html @@ -0,0 +1,18 @@ + + + + +Reports invalid or missing -Xmx configs in the Gitpod configuration. +

Example configuration:

+

+    jetbrains:
+      intellij:
+        vmoptions: -Xmx4g
+
+

More information: https://www.gitpod.io/docs/references/gitpod-yml#jetbrainsproductvmoptions

+ + diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources/messages/GitpodBundle.properties b/components/ide/jetbrains/backend-plugin/src/main/resources/messages/GitpodBundle.properties new file mode 100644 index 00000000000000..02a647d8960d40 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/resources/messages/GitpodBundle.properties @@ -0,0 +1,2 @@ +inspections.group.name=Gitpod +inspections.gitpod.schema.validation.name=Incorrect -Xmx config \ No newline at end of file