Skip to content
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 @@ -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};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -58,6 +63,11 @@ public String getDemoText() {
+ "\n"
+ "[Service]\n"
+ "Type=oneshot\n"
+ "RestrictAddressFamilies=<gOp>~</gOp><gEnum>AF_INET</gEnum> <gEnum>AF_INET6</gEnum>\n"
+ "SocketBindAllow=<gEnum>ipv4</gEnum><gOp>:</gOp><gEnum>tcp</gEnum><gOp>:</gOp><gLit>8080</gLit>\n"
+ "SocketBindDeny=<gEnum>any</gEnum>\n"
+ "IPAddressAllow=<gLit>192.168.1.1</gLit> <gLit>::1</gLit>\n"
+ "RootImagePolicy=<gEnum>root</gEnum><gOp>=</gOp><gEnum>verity</gEnum><gOp>+</gOp><gEnum>signed</gEnum>\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"
Expand All @@ -67,7 +77,12 @@ public String getDemoText() {
@Nullable
@Override
public Map<String, TextAttributesKey> getAdditionalHighlightingTagToDescriptorMap() {
return null;
Map<String, TextAttributesKey> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,7 @@ fun Combinator.colorize(value: String): List<Region> {
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 }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("/")

Expand Down Expand Up @@ -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,
Expand All @@ -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))))
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.PropertyIsNotInSectionAnnotator"/>
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.PidFileOptionWarning"/>
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.GrammarEngineKeyAnnotator"/>
<annotator language="Unit File (systemd)" implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.annotators.GrammarValueColorAnnotator"/>
<localInspection implementationClass="net.sjrx.intellij.plugins.systemdunitfiles.inspections.UnknownKeyInSectionInspection"
groupPath="Unit files (systemd)" language="Unit File (systemd)"
shortName="UnknownKeyInSection" displayName="Unknown option in section"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package net.sjrx.intellij.plugins.systemdunitfiles.annotators

import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.lang.annotation.HighlightSeverity
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
import net.sjrx.intellij.plugins.systemdunitfiles.settings.ExperimentalSettings
import org.junit.Test

/**
* End-to-end grammar value coloring (#467 / #342), behind the experimental flag. We assert that the
* expected spans get an INFORMATION-level coloring annotation (checked by the colored substring).
*/
class GrammarValueColorAnnotatorTest : AbstractUnitFileTest() {

override fun tearDown() {
try {
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = false
} finally {
super.tearDown()
}
}

private fun enableNewEngine() {
ExperimentalSettings.getInstance(project).state.useGrammarParseEngine = true
}

private fun colored(highlights: List<HighlightInfo>, 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"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading