diff --git a/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/UnitFileHighlighter.java b/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/UnitFileHighlighter.java
index e7eb364c..b2eb33c2 100644
--- a/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/UnitFileHighlighter.java
+++ b/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/UnitFileHighlighter.java
@@ -29,6 +29,16 @@ public class UnitFileHighlighter extends SyntaxHighlighterBase {
private static final TextAttributesKey BAD_CHARACTER
= createTextAttributesKey("UNIT_FILE_BAD_CHARACTER", HighlighterColors.BAD_CHARACTER);
+ // Grammar-driven value coloring (applied by an annotator, not the lexer) — one key per Role.
+ public static final TextAttributesKey GRAMMAR_ENUM
+ = createTextAttributesKey("UNIT_FILE_GRAMMAR_ENUM", DefaultLanguageHighlighterColors.CONSTANT);
+ public static final TextAttributesKey GRAMMAR_LITERAL
+ = createTextAttributesKey("UNIT_FILE_GRAMMAR_LITERAL", DefaultLanguageHighlighterColors.NUMBER);
+ public static final TextAttributesKey GRAMMAR_OPERATOR
+ = createTextAttributesKey("UNIT_FILE_GRAMMAR_OPERATOR", DefaultLanguageHighlighterColors.OPERATION_SIGN);
+ public static final TextAttributesKey GRAMMAR_IDENTIFIER
+ = createTextAttributesKey("UNIT_FILE_GRAMMAR_IDENTIFIER", DefaultLanguageHighlighterColors.IDENTIFIER);
+
private static final TextAttributesKey[] SECTION_KEYS = new TextAttributesKey[]{SECTION};
diff --git a/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/settings/UnitFileColorSettings.java b/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/settings/UnitFileColorSettings.java
index e900eb4e..3b129312 100644
--- a/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/settings/UnitFileColorSettings.java
+++ b/src/main/java/net/sjrx/intellij/plugins/systemdunitfiles/coloring/settings/UnitFileColorSettings.java
@@ -11,6 +11,7 @@
import org.jetbrains.annotations.Nullable;
import javax.swing.Icon;
+import java.util.HashMap;
import java.util.Map;
public class UnitFileColorSettings implements ColorSettingsPage {
@@ -21,6 +22,10 @@ public class UnitFileColorSettings implements ColorSettingsPage {
new AttributesDescriptor("Key", UnitFileHighlighter.KEY),
new AttributesDescriptor("Separator", UnitFileHighlighter.SEPARATOR),
new AttributesDescriptor("Value", UnitFileHighlighter.VALUE),
+ new AttributesDescriptor("Value//Enum (grammar)", UnitFileHighlighter.GRAMMAR_ENUM),
+ new AttributesDescriptor("Value//Literal (grammar)", UnitFileHighlighter.GRAMMAR_LITERAL),
+ new AttributesDescriptor("Value//Operator (grammar)", UnitFileHighlighter.GRAMMAR_OPERATOR),
+ new AttributesDescriptor("Value//Identifier (grammar)", UnitFileHighlighter.GRAMMAR_IDENTIFIER),
};
@Nullable
@@ -58,6 +63,11 @@ public String getDemoText() {
+ "\n"
+ "[Service]\n"
+ "Type=oneshot\n"
+ + "RestrictAddressFamilies=~AF_INET AF_INET6\n"
+ + "SocketBindAllow=ipv4:tcp:8080\n"
+ + "SocketBindDeny=any\n"
+ + "IPAddressAllow=192.168.1.1 ::1\n"
+ + "RootImagePolicy=root=verity+signed\n"
+ "ExecStartPre=-/usr/bin/systemctl daemon-reload\n"
+ "; we have to retrigger initrd-fs.target after daemon-reload\n"
+ "ExecStart=-/usr/bin/systemctl --no-block start initrd-fs.target\n"
@@ -67,7 +77,12 @@ public String getDemoText() {
@Nullable
@Override
public Map getAdditionalHighlightingTagToDescriptorMap() {
- return null;
+ Map tags = new HashMap<>();
+ tags.put("gEnum", UnitFileHighlighter.GRAMMAR_ENUM);
+ tags.put("gLit", UnitFileHighlighter.GRAMMAR_LITERAL);
+ tags.put("gOp", UnitFileHighlighter.GRAMMAR_OPERATOR);
+ tags.put("gId", UnitFileHighlighter.GRAMMAR_IDENTIFIER);
+ return tags;
}
@NotNull
diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/GrammarValueColorAnnotator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/GrammarValueColorAnnotator.kt
new file mode 100644
index 00000000..6ccc11cb
--- /dev/null
+++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/annotators/GrammarValueColorAnnotator.kt
@@ -0,0 +1,56 @@
+package net.sjrx.intellij.plugins.systemdunitfiles.annotators
+
+import com.intellij.lang.annotation.AnnotationHolder
+import com.intellij.lang.annotation.Annotator
+import com.intellij.lang.annotation.HighlightSeverity
+import com.intellij.openapi.editor.colors.TextAttributesKey
+import com.intellij.openapi.util.TextRange
+import com.intellij.psi.PsiElement
+import com.intellij.psi.util.PsiTreeUtil
+import net.sjrx.intellij.plugins.systemdunitfiles.coloring.UnitFileHighlighter
+import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileProperty
+import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionType
+import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.SemanticDataRepository
+import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.fileClass
+import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue
+import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Role
+import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.colorize
+import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
+
+/**
+ * Grammar-based value coloring (#467 / #342), behind the experimental flag.
+ *
+ * For options validated by the new grammar engine, [colorize] is asked which spans of the value map
+ * to which [Role], and each span is painted with the matching text-attributes key. Does nothing when
+ * the flag is off, so normal users are unaffected.
+ */
+class GrammarValueColorAnnotator : Annotator {
+
+ override fun annotate(element: PsiElement, holder: AnnotationHolder) {
+ if (element !is UnitFileProperty) return
+ if (!ExperimentalSettings.getInstance(element.project).state.useGrammarParseEngine) return
+
+ val section = PsiTreeUtil.getParentOfType(element, UnitFileSectionType::class.java) ?: return
+ val value = element.valueText ?: return
+ val base = element.valueNode?.psi?.textRange?.startOffset ?: return
+ val fileClass = element.containingFile.fileClass()
+ val validator = SemanticDataRepository.instance.getOptionValidator(fileClass, section.sectionName, element.key)
+ if (validator !is GrammarOptionValue) return
+
+ for (region in validator.combinator.colorize(value)) {
+ holder.newSilentAnnotation(HighlightSeverity.INFORMATION)
+ .range(TextRange(base + region.start, base + region.end))
+ .textAttributes(attributesFor(region.role))
+ .create()
+ }
+ }
+
+ companion object {
+ fun attributesFor(role: Role): TextAttributesKey = when (role) {
+ Role.ENUM -> UnitFileHighlighter.GRAMMAR_ENUM
+ Role.LITERAL -> UnitFileHighlighter.GRAMMAR_LITERAL
+ Role.OPERATOR -> UnitFileHighlighter.GRAMMAR_OPERATOR
+ Role.IDENTIFIER -> UnitFileHighlighter.GRAMMAR_IDENTIFIER
+ }
+ }
+}
diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Coloring.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Coloring.kt
index 87e80ac5..9c9cd5d7 100644
--- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Coloring.kt
+++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Coloring.kt
@@ -59,5 +59,7 @@ fun Combinator.colorize(value: String): List {
regions.add(Region(token.start, token.end, role))
}
}
- return regions.sortedBy { it.start }
+ // distinct(): nested Labeled (e.g. an IPv4 suffix inside an IPv6 address) can emit the same span
+ // twice; collapse exact duplicates.
+ return regions.distinct().sortedBy { it.start }
}
diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt
index 825f0c28..a3d30f91 100644
--- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt
+++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt
@@ -18,7 +18,8 @@ val TIME_VALUE = AlternativeCombinator(
var IPV4_OCTET = IntegerTerminal(0, 256)
val DOT = LiteralChoiceTerminal(".")
-var IPV4_ADDR = SequenceCombinator(IPV4_OCTET, DOT, IPV4_OCTET, DOT, IPV4_OCTET, DOT, IPV4_OCTET)
+// Labeled so an address colours as one literal span rather than per-octet/dot (transparent to matching).
+var IPV4_ADDR = Labeled(Role.LITERAL, SequenceCombinator(IPV4_OCTET, DOT, IPV4_OCTET, DOT, IPV4_OCTET, DOT, IPV4_OCTET))
val CIDR_SEPARATOR = LiteralChoiceTerminal("/")
@@ -51,7 +52,7 @@ val IPV6_IPV4_SUFFIX_FIVE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEX
//val IPV6_ALL_ZEROS = DOUBLE_COLON
-val IPV6_ADDR = AlternativeCombinator(
+val IPV6_ADDR = Labeled(Role.LITERAL, AlternativeCombinator(
IPV6_IPV4_SUFFIX_FULL,
IPV6_IPV4_SUFFIX_ZERO_HEXTET_BEFORE_ZERO_COMP,
IPV6_IPV4_SUFFIX_ONE_HEXTET_BEFORE_ZERO_COMP,
@@ -72,7 +73,7 @@ val IPV6_ADDR = AlternativeCombinator(
// I suspect maybe that this one is redundant
//IPV6_ALL_ZEROS,
-)
+))
val IPV6_ADDR_AND_PREFIX_LENGTH = SequenceCombinator(IPV6_ADDR, CIDR_SEPARATOR, IntegerTerminal(64, 129))
val IPV6_ADDR_AND_OPTIONAL_PREFIX_LENGTH = SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, IntegerTerminal(64, 129))))
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 2fdc75e5..49b186b6 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -61,6 +61,7 @@
+
, text: String) =
+ highlights.any { it.severity == HighlightSeverity.INFORMATION && it.text == text }
+
+ @Test
+ fun testSocketBindValueIsColoredByRole() {
+ enableNewEngine()
+ setupFileInEditor("file.service", "[Service]\nSocketBindAllow=ipv4:tcp:8080")
+
+ val highlights = myFixture.doHighlighting()
+ assertTrue(colored(highlights, "ipv4")) // ENUM
+ assertTrue(colored(highlights, ":")) // OPERATOR
+ assertTrue(colored(highlights, "8080")) // LITERAL
+ }
+
+ @Test
+ fun testIpAddressIsColoredAsOneLiteralSpan() {
+ enableNewEngine()
+ setupFileInEditor("file.network", "[Network]\nGateway=192.168.1.1")
+
+ // The whole address is one colored span (thanks to Labeled), not per-octet.
+ assertTrue(colored(myFixture.doHighlighting(), "192.168.1.1"))
+ }
+
+ @Test
+ fun testNoValueColoringWhenFlagOff() {
+ setupFileInEditor("file.service", "[Service]\nSocketBindAllow=ipv4:tcp:8080")
+
+ assertFalse(colored(myFixture.doHighlighting(), "8080"))
+ }
+}
diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ColoringTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ColoringTest.kt
index b800dde3..de899a76 100644
--- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ColoringTest.kt
+++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ColoringTest.kt
@@ -36,9 +36,15 @@ class ColoringTest {
@Test
fun testLabeledPaintsACompositeSpanAsOneUnit() {
- // Without Labeled an IPv4 address would colour per octet/dot; wrapping it makes it one LITERAL.
- val grammar = SequenceCombinator(Labeled(Role.LITERAL, IPV4_ADDR), EOF())
- assertEquals(listOf(Region(0, 7, Role.LITERAL)), grammar.colorize("1.2.3.4"))
+ // Without Labeled these would colour as two ENUM tokens; wrapping makes the span one LITERAL.
+ val grammar = Labeled(Role.LITERAL, SequenceCombinator(LiteralChoiceTerminal("a"), LiteralChoiceTerminal("b")))
+ assertEquals(listOf(Region(0, 2, Role.LITERAL)), grammar.colorize("ab"))
+ }
+
+ @Test
+ fun testSharedIpCombinatorIsLabeledAsOneLiteral() {
+ // IPV4_ADDR is wrapped in Labeled in Combinators.kt, so an address colours as one literal.
+ assertEquals(listOf(Region(0, 7, Role.LITERAL)), SequenceCombinator(IPV4_ADDR, EOF()).colorize("1.2.3.4"))
}
@Test