Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jb] add inspections & quickfixes on .gitpod.yml vmoptions config #11089

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<plugin id="Git4Idea"/>
<plugin id="org.jetbrains.plugins.terminal"/>
<plugin id="com.jetbrains.codeWithMe"/>
<plugin id="org.jetbrains.plugins.yaml"/>
</dependencies>

<extensions defaultExtensionNs="com.intellij">
Expand All @@ -30,6 +31,13 @@
<projectService serviceImplementation="io.gitpod.jetbrains.remote.GitpodClientProjectSessionTracker" client="guest" preload="true"/>
<projectService serviceImplementation="io.gitpod.jetbrains.remote.GitpodProjectManager" preload="true"/>
<gateway.customization.name implementation="io.gitpod.jetbrains.remote.GitpodGatewayClientCustomizationProvider"/>
<localInspection language="yaml" bundle="messages.GitpodBundle"
shortName="GitpodConfigInspection"
groupKey="inspections.group.name"
key="inspections.gitpod.schema.validation.name"
level="WARNING" enabledByDefault="true"
implementationClass="io.gitpod.jetbrains.remote.inspections.GitpodConfigInspection"
/>
</extensions>

</idea-plugin>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!--
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.
-->

<html>
<body>
Reports invalid or missing -Xmx configs in the Gitpod configuration.
<p><b>Example configuration:</b></p>
<pre><code>
jetbrains:
intellij:
vmoptions: -Xmx4g
</code></pre>
<p>More information: <a href="https://www.gitpod.io/docs/references/gitpod-yml#jetbrainsproductvmoptions">https://www.gitpod.io/docs/references/gitpod-yml#jetbrainsproductvmoptions</a></p>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
inspections.group.name=Gitpod
inspections.gitpod.schema.validation.name=Incorrect -Xmx config