Skip to content

Commit 2d35028

Browse files
committed
feat(jb): add inspections & quickfixes on .gitpod.yml vmoptions config
1 parent 65384db commit 2d35028

File tree

9 files changed

+256
-1
lines changed

9 files changed

+256
-1
lines changed

components/ide/jetbrains/backend-plugin/gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ platformType=IU
1010
platformDownloadSources=true
1111
# Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html
1212
# Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22
13-
platformPlugins=Git4Idea, org.jetbrains.plugins.terminal, com.jetbrains.codeWithMe
13+
platformPlugins=Git4Idea, org.jetbrains.plugins.terminal, com.jetbrains.codeWithMe, org.jetbrains.plugins.yaml
1414
# Opt-out flag for bundling Kotlin standard library.
1515
# See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details.
1616
kotlin.stdlib.default.dependency=false
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.jetbrains.remote.inspections
6+
7+
import com.intellij.codeInspection.LocalInspectionTool
8+
import com.intellij.codeInspection.ProblemsHolder
9+
import com.intellij.diagnostic.VMOptions
10+
import com.intellij.openapi.util.BuildNumber
11+
import com.intellij.psi.PsiElementVisitor
12+
import com.intellij.psi.PsiFile
13+
import io.gitpod.jetbrains.remote.quickfixes.AddVMOptionsQuickFix
14+
import io.gitpod.jetbrains.remote.quickfixes.ApplyVMOptionsQuickFix
15+
import io.gitpod.jetbrains.remote.quickfixes.ReplaceVMOptionsQuickFix
16+
import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey
17+
import io.gitpod.jetbrains.remote.utils.GitpodConfig.defaultXmxMiB
18+
import io.gitpod.jetbrains.remote.utils.GitpodConfig.getJetBrainsProductName
19+
import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlFile
20+
import org.jetbrains.yaml.YAMLUtil
21+
import org.jetbrains.yaml.psi.YAMLFile
22+
import org.jetbrains.yaml.psi.YAMLKeyValue
23+
24+
class GitpodConfigInspection : LocalInspectionTool() {
25+
26+
private val runtimeXmxMiB = Runtime.getRuntime().maxMemory().shr(20)
27+
28+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
29+
return object : PsiElementVisitor() {
30+
override fun visitFile(file: PsiFile) {
31+
if (file.name != gitpodYamlFile || file !is YAMLFile) return
32+
val productCode = BuildNumber.currentVersion().productCode
33+
val productName = getJetBrainsProductName(productCode) ?: return
34+
val keyValue = YAMLUtil.getQualifiedKeyInFile(file, YamlKey.jetbrains, productName, YamlKey.vmOptions)
35+
if (keyValue == null) {
36+
holder.registerProblem(
37+
file,
38+
"IDE's max heap size (-Xmx) is ${runtimeXmxMiB}m, but not configured in $gitpodYamlFile",
39+
AddVMOptionsQuickFix(productName, runtimeXmxMiB)
40+
)
41+
return
42+
}
43+
val configuredXmxMiB = getUserConfiguredXmxValue(keyValue)
44+
if (configuredXmxMiB == null && runtimeXmxMiB != defaultXmxMiB) {
45+
val description = "IDE's max heap size (-Xmx) is ${runtimeXmxMiB}m, but not configured in $gitpodYamlFile"
46+
val fix = if (VMOptions.canWriteOptions()) {
47+
ApplyVMOptionsQuickFix("Apply default -Xmx${defaultXmxMiB}m", defaultXmxMiB)
48+
} else {
49+
ReplaceVMOptionsQuickFix(runtimeXmxMiB)
50+
}
51+
holder.registerProblem(file, description, fix)
52+
return
53+
}
54+
if (configuredXmxMiB != null && configuredXmxMiB != runtimeXmxMiB) {
55+
val description = "IDE's max heap size (-Xmx) is ${runtimeXmxMiB}m, but -Xmx${configuredXmxMiB}m configured in $gitpodYamlFile"
56+
val fix = if (VMOptions.canWriteOptions()) {
57+
ApplyVMOptionsQuickFix("Apply -Xmx${configuredXmxMiB}m configured in $gitpodYamlFile", runtimeXmxMiB)
58+
} else {
59+
ReplaceVMOptionsQuickFix(runtimeXmxMiB)
60+
}
61+
holder.registerProblem(file, description, fix)
62+
}
63+
}
64+
}
65+
}
66+
67+
private fun getUserConfiguredXmxValue(vmOptionsKeyValue: YAMLKeyValue): Long? {
68+
val vmOptions = vmOptionsKeyValue.valueText.trim().split("\\s".toRegex())
69+
// the rightmost option is the one to take effect
70+
val finalXmx = vmOptions.lastOrNull { it.startsWith("-Xmx") } ?: return null
71+
val xmxValue = finalXmx.substringAfter("-Xmx")
72+
return try {
73+
VMOptions.parseMemoryOption(xmxValue).shr(20)
74+
} catch (e: IllegalArgumentException) {
75+
// ignore invalid user configuration
76+
null
77+
}
78+
}
79+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.jetbrains.remote.quickfixes
6+
7+
import com.intellij.codeInspection.LocalQuickFix
8+
import com.intellij.codeInspection.ProblemDescriptor
9+
import com.intellij.openapi.diagnostic.thisLogger
10+
import com.intellij.openapi.project.Project
11+
import com.intellij.psi.PsiDocumentManager
12+
import com.intellij.psi.PsiElement
13+
import com.intellij.psi.codeStyle.CodeStyleManager
14+
import com.intellij.psi.util.PsiTreeUtil
15+
import com.intellij.util.IncorrectOperationException
16+
import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey
17+
import io.gitpod.jetbrains.remote.utils.GitpodConfig.gitpodYamlFile
18+
import org.jetbrains.yaml.YAMLElementGenerator
19+
import org.jetbrains.yaml.psi.YAMLFile
20+
import org.jetbrains.yaml.psi.YAMLKeyValue
21+
22+
class AddVMOptionsQuickFix(private val productName: String, private val xmxValueMiB: Long) : LocalQuickFix {
23+
24+
override fun getName() = "Add -Xmx${xmxValueMiB}m to $gitpodYamlFile"
25+
26+
override fun getFamilyName() = name
27+
28+
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
29+
val psiFile = descriptor.psiElement as? YAMLFile ?: return
30+
val document = psiFile.viewProvider.document ?: return
31+
val generator = YAMLElementGenerator.getInstance(project)
32+
val jetbrainsKeyValue = findOrCreateYamlKeyValue(psiFile, YamlKey.jetbrains, "", generator) ?: return
33+
val productKeyValue = findOrCreateYamlKeyValue(jetbrainsKeyValue, productName, "", generator) ?: return
34+
findOrCreateYamlKeyValue(productKeyValue, YamlKey.vmOptions, "-Xmx${xmxValueMiB}m", generator)
35+
PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(document)
36+
try {
37+
CodeStyleManager.getInstance(project).reformat(jetbrainsKeyValue)
38+
} catch (e: IncorrectOperationException) {
39+
thisLogger().warn("AddVMOptionsQuickFix reformat failed", e)
40+
}
41+
}
42+
43+
private fun findOrCreateYamlKeyValue(
44+
parent: PsiElement,
45+
keyText: String,
46+
valueText: String,
47+
generator: YAMLElementGenerator
48+
): PsiElement? {
49+
var element = findElementByYamlKeyText(parent, keyText)
50+
return if (element == null) {
51+
element = generator.createYamlKeyValue(keyText, valueText)
52+
parent.add(generator.createEol())
53+
parent.add(element) ?: return null
54+
} else {
55+
element
56+
}
57+
}
58+
59+
private fun findElementByYamlKeyText(rootElement: PsiElement, keyText: String): PsiElement? {
60+
return PsiTreeUtil.collectElements(rootElement) {
61+
it is YAMLKeyValue && it.keyText == keyText
62+
}.firstOrNull()
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.jetbrains.remote.quickfixes
6+
7+
import com.intellij.codeInspection.LocalQuickFix
8+
import com.intellij.codeInspection.ProblemDescriptor
9+
import com.intellij.diagnostic.VMOptions
10+
import com.intellij.openapi.project.Project
11+
12+
class ApplyVMOptionsQuickFix(private val quickFixName: String, private val xmxValueMiB: Long) : LocalQuickFix {
13+
14+
override fun getName() = quickFixName
15+
16+
override fun getFamilyName() = name
17+
18+
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
19+
VMOptions.setOption(VMOptions.MemoryKind.HEAP, xmxValueMiB.toInt())
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.jetbrains.remote.quickfixes
6+
7+
import com.intellij.codeInspection.LocalQuickFix
8+
import com.intellij.codeInspection.ProblemDescriptor
9+
import com.intellij.openapi.project.Project
10+
import io.gitpod.jetbrains.remote.utils.GitpodConfig.YamlKey
11+
import org.jetbrains.yaml.YAMLElementGenerator
12+
import org.jetbrains.yaml.psi.YAMLKeyValue
13+
14+
class ReplaceVMOptionsQuickFix(private val xmxValueMiB: Long) : LocalQuickFix {
15+
16+
override fun getName() = "Set Xmx to $xmxValueMiB MiB"
17+
18+
override fun getFamilyName() = name
19+
20+
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
21+
val vmOptionsKeyValue = descriptor.psiElement as? YAMLKeyValue ?: return
22+
if (vmOptionsKeyValue.keyText != YamlKey.vmOptions) return
23+
val vmOptions = vmOptionsKeyValue.valueText.trim().split("\\s".toRegex())
24+
val xmxUpdated = "-Xmx${xmxValueMiB}m"
25+
val xmxOptions = vmOptions
26+
.filter { it.startsWith("-Xmx") }
27+
.map { xmxUpdated }
28+
.ifEmpty { listOf(xmxUpdated) }
29+
val nonXmxOptions = vmOptions
30+
.filter { !it.startsWith("-Xmx") }
31+
val newVmOptions = (xmxOptions + nonXmxOptions).toSortedSet().joinToString(" ")
32+
val generator = YAMLElementGenerator.getInstance(project)
33+
val psiElementUpdated = generator.createYamlKeyValue(YamlKey.vmOptions, newVmOptions)
34+
vmOptionsKeyValue.replace(psiElementUpdated)
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package io.gitpod.jetbrains.remote.utils
6+
7+
/**
8+
* Constants and util functions for Gitpod config spec
9+
*/
10+
object GitpodConfig {
11+
12+
// FIXME: get from env var
13+
const val defaultXmxMiB = 2048L
14+
const val gitpodYamlFile = ".gitpod.yml"
15+
16+
object YamlKey {
17+
const val jetbrains = "jetbrains"
18+
const val vmOptions = "vmoptions"
19+
}
20+
21+
/**
22+
* map JetBrains IDE productCode to YAML key for .gitpod.yml
23+
*/
24+
fun getJetBrainsProductName(productCode: String): String? {
25+
return when (productCode) {
26+
"IC" -> "intellij"
27+
"IU" -> "intellij"
28+
"PS" -> "phpstorm"
29+
"PY" -> "pycharm"
30+
"GO" -> "goland"
31+
else -> null
32+
}
33+
}
34+
}

components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml

+7
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<plugin id="Git4Idea"/>
2020
<plugin id="org.jetbrains.plugins.terminal"/>
2121
<plugin id="com.jetbrains.codeWithMe"/>
22+
<plugin id="org.jetbrains.plugins.yaml"/>
2223
</dependencies>
2324

2425
<extensions defaultExtensionNs="com.intellij">
@@ -29,6 +30,12 @@
2930
<projectService serviceImplementation="io.gitpod.jetbrains.remote.GitpodClientProjectSessionTracker" client="guest" preload="true"/>
3031
<projectService serviceImplementation="io.gitpod.jetbrains.remote.GitpodProjectManager" preload="true"/>
3132
<gateway.customization.name implementation="io.gitpod.jetbrains.remote.GitpodGatewayClientCustomizationProvider"/>
33+
<localInspection language="yaml" bundle="messages.GitpodBundle"
34+
groupKey="inspections.group.name"
35+
key="inspections.gitpod.schema.validation.name"
36+
level="WARNING" enabledByDefault="true"
37+
implementationClass="io.gitpod.jetbrains.remote.inspections.GitpodConfigInspection"
38+
/>
3239
</extensions>
3340

3441
</idea-plugin>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<html>
2+
<body>
3+
Reports invalid or missing -Xmx configs in the Gitpod configuration.
4+
<p><b>Example configuration:</b></p>
5+
<pre><code>
6+
jetbrains:
7+
intellij:
8+
vmoptions: -Xmx4g
9+
</code></pre>
10+
<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>
11+
</body>
12+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
inspections.group.name=Gitpod
2+
inspections.gitpod.schema.validation.name=Incorrect -Xmx config

0 commit comments

Comments
 (0)