Skip to content

Commit b09b1ce

Browse files
Steve Ramageclaude
andcommitted
feat: underline grammar-engine-backed keys behind a dedicated flag (#467)
A debug aid for the grammar engine work: while enabled, underline the KEY of every option whose value is backed by a GrammarOptionValue, so it's obvious at a glance which keys the new engine covers. The key keeps its normal color and just gets a gray underline — recoloring grammar vs non-grammar keys was too distracting, especially on light themes. Gated on its own experimental flag (underlineGrammarEngineKeys, off by default), independent of useGrammarParseEngine: the grammar validators exist in the registry regardless of which validation path is active, so this toggles on its own. Surfaced as a third checkbox on the settings page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a1daaf9 commit b09b1ce

5 files changed

Lines changed: 128 additions & 3 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.annotators
2+
3+
import com.intellij.lang.annotation.AnnotationHolder
4+
import com.intellij.lang.annotation.Annotator
5+
import com.intellij.lang.annotation.HighlightSeverity
6+
import com.intellij.openapi.editor.colors.TextAttributesKey
7+
import com.intellij.openapi.editor.markup.EffectType
8+
import com.intellij.openapi.editor.markup.TextAttributes
9+
import com.intellij.psi.PsiElement
10+
import com.intellij.psi.util.PsiTreeUtil
11+
import com.intellij.ui.JBColor
12+
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileProperty
13+
import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionType
14+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.SemanticDataRepository
15+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.fileClass
16+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue
17+
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
18+
import java.awt.Font
19+
20+
/**
21+
* Debug aid (#467): while the "underline grammar-engine keys" flag is enabled, underline the KEY of
22+
* every option whose value is validated by a [GrammarOptionValue], so it is obvious at a glance which
23+
* keys are backed by the new grammar engine versus the original SyntacticMatch/SemanticMatch path.
24+
*
25+
* Gated on its own flag ([ExperimentalSettings.State.underlineGrammarEngineKeys]), independent of
26+
* whether the grammar engine is the active validation path — the grammar validators exist in the
27+
* registry either way. Does nothing when the flag is off, so it has no effect for normal users.
28+
*/
29+
class GrammarEngineKeyAnnotator : Annotator {
30+
31+
override fun annotate(element: PsiElement, holder: AnnotationHolder) {
32+
if (element !is UnitFileProperty) return
33+
if (!ExperimentalSettings.getInstance(element.project).state.underlineGrammarEngineKeys) return
34+
35+
val section = PsiTreeUtil.getParentOfType(element, UnitFileSectionType::class.java) ?: return
36+
val fileClass = element.containingFile.fileClass()
37+
val validator = SemanticDataRepository.instance.getOptionValidator(fileClass, section.sectionName, element.key)
38+
39+
if (validator is GrammarOptionValue) {
40+
holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
41+
.range(element.keyNode.psi)
42+
.textAttributes(NEW_ENGINE_KEY)
43+
.create()
44+
}
45+
}
46+
47+
companion object {
48+
// Underline the key, keeping its normal color — recoloring grammar vs non-grammar keys with two
49+
// different colors was too distracting, especially on light themes.
50+
val NEW_ENGINE_KEY: TextAttributesKey = TextAttributesKey.createTextAttributesKey(
51+
"SYSTEMD_UNIT_FILE_NEW_GRAMMAR_ENGINE_KEY",
52+
TextAttributes(null, null, JBColor.GRAY, EffectType.LINE_UNDERSCORE, Font.PLAIN),
53+
)
54+
}
55+
}

src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/settings/ExperimentalSettings.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ class ExperimentalSettings : PersistentStateComponent<ExperimentalSettings.State
2424
* validation instead of the original SyntacticMatch/SemanticMatch path.
2525
*/
2626
var useGrammarParseEngine: Boolean = false
27+
28+
/**
29+
* Underline the KEY of every option whose value is backed by a grammar validator
30+
* ([GrammarOptionValue]), a debug aid for seeing which keys the new engine covers. Independent of
31+
* [useGrammarParseEngine]: the grammar validators exist in the registry regardless of which
32+
* validation path is active, so this can be toggled on its own.
33+
*/
34+
var underlineGrammarEngineKeys: Boolean = false
2735
}
2836

2937
override fun getState(): State = myState

src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/settings/PodmanQuadletConfigurable.kt

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class PodmanQuadletConfigurable(private val project: Project) : Configurable {
1111

1212
private var enabledCheckbox: JBCheckBox? = null
1313
private var grammarEngineCheckbox: JBCheckBox? = null
14+
private var underlineGrammarKeysCheckbox: JBCheckBox? = null
1415

1516
override fun getDisplayName(): String = "systemd Unit Files"
1617

@@ -22,10 +23,15 @@ class PodmanQuadletConfigurable(private val project: Project) : Configurable {
2223
"Use the new grammar engine for value validation (experimental)",
2324
experimental.state.useGrammarParseEngine,
2425
)
26+
underlineGrammarKeysCheckbox = JBCheckBox(
27+
"Underline keys backed by the new grammar engine (experimental)",
28+
experimental.state.underlineGrammarEngineKeys,
29+
)
2530

2631
return FormBuilder.createFormBuilder()
2732
.addComponent(enabledCheckbox!!)
2833
.addComponent(grammarEngineCheckbox!!)
34+
.addComponent(underlineGrammarKeysCheckbox!!)
2935
.addComponentFillVertically(JPanel(), 0)
3036
.panel
3137
}
@@ -34,7 +40,8 @@ class PodmanQuadletConfigurable(private val project: Project) : Configurable {
3440
val settings = PodmanQuadletSettings.getInstance(project)
3541
val experimental = ExperimentalSettings.getInstance(project)
3642
return enabledCheckbox?.isSelected != settings.state.enabled ||
37-
grammarEngineCheckbox?.isSelected != experimental.state.useGrammarParseEngine
43+
grammarEngineCheckbox?.isSelected != experimental.state.useGrammarParseEngine ||
44+
underlineGrammarKeysCheckbox?.isSelected != experimental.state.underlineGrammarEngineKeys
3845
}
3946

4047
override fun apply() {
@@ -45,12 +52,16 @@ class PodmanQuadletConfigurable(private val project: Project) : Configurable {
4552
}
4653
settings.state.enabled = newEnabled
4754

48-
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = grammarEngineCheckbox?.isSelected ?: false
55+
val experimental = ExperimentalSettings.getInstance(project)
56+
experimental.state.useGrammarParseEngine = grammarEngineCheckbox?.isSelected ?: false
57+
experimental.state.underlineGrammarEngineKeys = underlineGrammarKeysCheckbox?.isSelected ?: false
4958
}
5059

5160
override fun reset() {
5261
val settings = PodmanQuadletSettings.getInstance(project)
62+
val experimental = ExperimentalSettings.getInstance(project)
5363
enabledCheckbox?.isSelected = settings.state.enabled
54-
grammarEngineCheckbox?.isSelected = ExperimentalSettings.getInstance(project).state.useGrammarParseEngine
64+
grammarEngineCheckbox?.isSelected = experimental.state.useGrammarParseEngine
65+
underlineGrammarKeysCheckbox?.isSelected = experimental.state.underlineGrammarEngineKeys
5566
}
5667
}

src/main/resources/META-INF/plugin.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.PidFileOptionWarning"/>
6363
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.DeprecatedGrammarValueAnnotator"/>
6464
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.GrammarValueColorAnnotator"/>
65+
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.GrammarEngineKeyAnnotator"/>
6566
<localInspection implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.inspections.UnknownKeyInSectionInspection"
6667
groupPath="Unit files (systemd)" language="Unit File (systemd)"
6768
shortName="UnknownKeyInSection" displayName="Unknown option in section"
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.annotators
2+
3+
import com.intellij.lang.annotation.HighlightSeverity
4+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
5+
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
6+
import org.junit.Test
7+
8+
class GrammarEngineKeyAnnotatorTest : AbstractUnitFileTest() {
9+
10+
// The light-test project is shared across classes, so don't leak the opt-in.
11+
override fun tearDown() {
12+
try {
13+
ExperimentalSettings.getInstance(project).state.underlineGrammarEngineKeys = false
14+
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = false
15+
} finally {
16+
super.tearDown()
17+
}
18+
}
19+
20+
private fun markedKey(highlights: List<com.intellij.codeInsight.daemon.impl.HighlightInfo>, key: String) =
21+
highlights.any { it.severity == HighlightSeverity.INFORMATION && it.text == key }
22+
23+
@Test
24+
fun testGrammarBackedKeyIsMarkedWhenFlagOn() {
25+
ExperimentalSettings.getInstance(project).state.underlineGrammarEngineKeys = true
26+
// RestrictAddressFamilies is validated by a GrammarOptionValue.
27+
setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_INET\n")
28+
29+
val highlights = myFixture.doHighlighting()
30+
assertTrue(markedKey(highlights, "RestrictAddressFamilies"))
31+
}
32+
33+
@Test
34+
fun testNotMarkedWhenFlagOff() {
35+
setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_INET\n")
36+
37+
val highlights = myFixture.doHighlighting()
38+
assertFalse(markedKey(highlights, "RestrictAddressFamilies"))
39+
}
40+
41+
@Test
42+
fun testIndependentOfParseEngineFlag() {
43+
// The underline flag drives this annotator on its own; turning on only the parse engine must not
44+
// mark keys, and turning on only the underline flag must (proving the two flags are decoupled).
45+
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = true
46+
setupFileInEditor("file.service", "[Service]\nRestrictAddressFamilies=AF_INET\n")
47+
48+
assertFalse(markedKey(myFixture.doHighlighting(), "RestrictAddressFamilies"))
49+
}
50+
}

0 commit comments

Comments
 (0)