/*
* Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC
* All rights reserved.
*
* The source code of this document is proprietary work, and is not licensed for
* distribution. For information about licensing, contact Sam Harwell at:
* sam@tunnelvisionlabs.com
*/
package org.antlr.works.editor.grammar.highlighter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.text.AttributeSet;
import javax.swing.text.StyledDocument;
import org.antlr.netbeans.editor.text.DocumentSnapshot;
import org.antlr.netbeans.editor.text.OffsetRegion;
import org.antlr.netbeans.parsing.spi.ParserData;
import org.antlr.v4.runtime.Dependents;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.RuleDependencies;
import org.antlr.v4.runtime.RuleDependency;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.Tuple2;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.works.editor.antlr4.semantics.AbstractParseTreeSemanticHighlighter;
import org.antlr.works.editor.antlr4.semantics.AbstractSemanticHighlighter;
import org.antlr.works.editor.grammar.GrammarEditorKit;
import org.antlr.works.editor.grammar.GrammarParserDataDefinitions;
import org.antlr.works.editor.grammar.experimental.GrammarParser;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.ArgActionParameterContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.AtomContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.BlockContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.ElementContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.ElementOptionContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.ElementOptionsContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.GrammarTypeContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.IdContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerAtomContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerCommandContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerCommandExprContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerCommandNameContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LocalsSpecContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.ModeSpecContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.OptionContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.RuleReturnsContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.RuleSpecContext;
import org.antlr.works.editor.grammar.experimental.generated.GrammarParserBaseListener;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.settings.FontColorSettings;
import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory;
import org.netbeans.spi.editor.highlighting.support.OffsetsBag;
import org.openide.util.Lookup;
/**
*
* @author Sam Harwell
*/
public class SemanticHighlighter extends AbstractParseTreeSemanticHighlighter<SemanticHighlighter.SemanticAnalyzerListener, ParserRuleContext> {
private final AttributeSet parameterDeclarationAttributes;
private final AttributeSet returnValueDeclarationAttributes;
private final AttributeSet localDeclarationAttributes;
private final AttributeSet invalidOptionAttributes;
private final AttributeSet lexerCommandAttributes;
private final AttributeSet lexerModeAttributes;
private SemanticHighlighter(@NonNull StyledDocument document) {
super(document, GrammarParserDataDefinitions.REFERENCE_PARSE_TREE);
Lookup lookup = MimeLookup.getLookup(MimePath.parse(GrammarEditorKit.GRAMMAR_MIME_TYPE));
FontColorSettings settings = lookup.lookup(FontColorSettings.class);
this.parameterDeclarationAttributes = getFontAndColors(settings, "definition");
this.returnValueDeclarationAttributes = getFontAndColors(settings, "definition");
this.localDeclarationAttributes = getFontAndColors(settings, "definition");
this.invalidOptionAttributes = getFontAndColors(settings, "invalidoption");
this.lexerCommandAttributes = getFontAndColors(settings, "lexerCommand");
this.lexerModeAttributes = getFontAndColors(settings, "lexerMode");
}
@Override
protected SemanticAnalyzerListener createListener(ParserData<? extends ParserRuleContext> parserData) {
return new SemanticAnalyzerListener();
}
@Override
protected ParseTree getParseTree(ParserData<? extends ParserRuleContext> parserData) {
return parserData.getData();
}
@Override
protected void updateHighlights(OffsetsBag targetContainer, DocumentSnapshot sourceSnapshot, DocumentSnapshot currentSnapshot, SemanticAnalyzerListener listener) {
List<Tuple2<OffsetRegion, AttributeSet>> intermediateContainer = new ArrayList<>();
addHighlights(intermediateContainer, sourceSnapshot, currentSnapshot, listener.getParameterDeclarations(), parameterDeclarationAttributes);
addHighlights(intermediateContainer, sourceSnapshot, currentSnapshot, listener.getReturnValueDeclarations(), returnValueDeclarationAttributes);
addHighlights(intermediateContainer, sourceSnapshot, currentSnapshot, listener.getLocalsDeclarations(), localDeclarationAttributes);
addHighlights(intermediateContainer, sourceSnapshot, currentSnapshot, listener.getInvalidOptions(), invalidOptionAttributes);
addHighlights(intermediateContainer, sourceSnapshot, currentSnapshot, listener.getLexerCommands(), lexerCommandAttributes);
addHighlights(intermediateContainer, sourceSnapshot, currentSnapshot, listener.getLexerModes(), lexerModeAttributes);
OffsetsBag container = new OffsetsBag(currentSnapshot.getVersionedDocument().getDocument());
fillHighlights(container, intermediateContainer);
targetContainer.setHighlights(container);
}
@MimeRegistration(mimeType=GrammarEditorKit.GRAMMAR_MIME_TYPE, service=HighlightsLayerFactory.class)
public static class LayerFactory extends AbstractLayerFactory {
public LayerFactory() {
super(SemanticHighlighter.class);
}
@Override
protected AbstractSemanticHighlighter<?> createHighlighter(Context context) {
return new SemanticHighlighter((StyledDocument)context.getDocument());
}
}
public static class SemanticAnalyzerListener extends GrammarParserBaseListener {
private static final Set<String> knownBlockOptions = new HashSet<String>() {{
add("greedy");
add("simrecursion_");
}};
private static final Set<String> knownRuleOptions = new HashSet<String>() {{
add("context");
add("simrecursion_");
}};
private static final Set<String> knownLexerGrammarOptions = new HashSet<String>() {{
add("language");
add("tokenVocab");
add("TokenLabelType");
add("superClass");
add("filter");
add("abstract");
}};
private static final Set<String> knownParserGrammarOptions = new HashSet<String>() {{
add("tokenVocab");
add("TokenLabelType");
add("superClass");
add("abstract");
}};
private static final Set<String> knownCombinedGrammarOptions = new HashSet<String>() {{
add("language");
add("tokenVocab");
add("TokenLabelType");
add("superClass");
add("filter");
add("abstract");
}};
/**
* These are known values for an {@link ElementOptionContext} appearing
* on a terminal reference within a parser rule in the grammar.
*/
private static final Set<String> KnownTerminalOptions = new HashSet<String>() {{
}};
/**
* These are known values for an {@link ElementOptionContext} appearing
* on a {@link SetElementContext}.
*/
private static final Set<String> KnownSetElementOptions = new HashSet<String>() {{
}};
/**
* These are known values for an {@link ElementOptionContext} appearing
* on an {@link AtomContext}.
*/
private static final Set<String> KnownAtomOptions = new HashSet<String>() {{
}};
/**
* These are known values for an {@link ElementOptionContext} appearing
* on a {@link LexerAtomCOntext}.
*/
private static final Set<String> KnownLexerAtomOptions = new HashSet<String>() {{
}};
/**
* These are known values for an {@link ElementOptionContext} appearing
* on an alternative.
*/
private static final Set<String> KnownAlternativeOptions = new HashSet<String>() {{
add("assoc");
}};
/**
* These are known values for an {@link ElementOptionContext} appearing
* on an embedded action.
*/
private static final Set<String> KnownEmbeddedActionOptions = new HashSet<String>() {{
}};
/**
* These are known values for an {@link ElementOptionContext} appearing
* on a semantic predicate.
*/
private static final Set<String> KnownSemanticPredicateOptions = new HashSet<String>() {{
add("fail");
}};
/**
* These are known values for an {@link ElementOptionContext} appearing
* on a rule reference (non-terminal in the parser).
*/
private static final Set<String> KnownRuleReferenceOptions = new HashSet<String>() {{
add("p");
}};
private final Deque<Integer> memberContext = new ArrayDeque<>();
private final List<Token> parameterDeclarations = new ArrayList<>();
private final List<Token> returnValueDeclarations = new ArrayList<>();
private final List<Token> localsDeclarations = new ArrayList<>();
private final List<Token> invalidOptions = new ArrayList<>();
private final List<Token> lexerCommands = new ArrayList<>();
private final List<Token> lexerModes = new ArrayList<>();
private final Map<String, List<Token>> possibleLexerModes = new HashMap<>();
private final HashSet<String> lexerModeNames = new HashSet<>();
private int grammarType;
private int ruleLevel;
private int blockLevel;
private int inNamedModeCommand;
public List<Token> getParameterDeclarations() {
return parameterDeclarations;
}
public List<Token> getReturnValueDeclarations() {
return returnValueDeclarations;
}
public List<Token> getLocalsDeclarations() {
return localsDeclarations;
}
public List<Token> getInvalidOptions() {
return invalidOptions;
}
public List<Token> getLexerCommands() {
return lexerCommands;
}
public List<Token> getLexerModes() {
return lexerModes;
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_grammarType, version=0, dependents=Dependents.PARENTS)
public void enterGrammarType(GrammarTypeContext ctx) {
if (ctx.LEXER() != null) {
grammarType = GrammarParser.LEXER;
} else if (ctx.PARSER() != null) {
grammarType = GrammarParser.PARSER;
} else {
grammarType = GrammarParser.COMBINED;
}
}
@Override
@RuleDependencies({
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_argActionParameter, version=7, dependents={Dependents.PARENTS, Dependents.ANCESTORS}),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleSpec, version=7, dependents=Dependents.DESCENDANTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleReturns, version=7, dependents=Dependents.DESCENDANTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_localsSpec, version=7, dependents=Dependents.DESCENDANTS),
})
public void enterArgActionParameter(ArgActionParameterContext ctx) {
if (ctx.name != null) {
int context = memberContext.isEmpty() ? GrammarParser.RULE_ruleSpec : memberContext.peek();
switch (context) {
case GrammarParser.RULE_ruleReturns:
returnValueDeclarations.add(ctx.name);
break;
case GrammarParser.RULE_localsSpec:
localsDeclarations.add(ctx.name);
break;
case GrammarParser.RULE_ruleSpec:
default:
parameterDeclarations.add(ctx.name);
break;
}
}
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleSpec, version=3, dependents=Dependents.PARENTS)
public void enterRuleSpec(RuleSpecContext ctx) {
memberContext.push(GrammarParser.RULE_ruleSpec);
ruleLevel++;
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleSpec, version=3, dependents=Dependents.PARENTS)
public void exitRuleSpec(RuleSpecContext ctx) {
int context = memberContext.pop();
assert context == GrammarParser.RULE_ruleSpec;
ruleLevel--;
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleReturns, version=0, dependents=Dependents.PARENTS)
public void enterRuleReturns(RuleReturnsContext ctx) {
memberContext.push(GrammarParser.RULE_ruleReturns);
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleReturns, version=0, dependents=Dependents.PARENTS)
public void exitRuleReturns(RuleReturnsContext ctx) {
int context = memberContext.pop();
assert context == GrammarParser.RULE_ruleReturns;
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_localsSpec, version=0, dependents=Dependents.PARENTS)
public void enterLocalsSpec(LocalsSpecContext ctx) {
memberContext.push(GrammarParser.RULE_localsSpec);
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_localsSpec, version=0, dependents=Dependents.PARENTS)
public void exitLocalsSpec(LocalsSpecContext ctx) {
int context = memberContext.pop();
assert context == GrammarParser.RULE_localsSpec;
}
@Override
@RuleDependencies({
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_modeSpec, version=3, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_id, version=1, dependents=Dependents.DESCENDANTS),
})
public void enterModeSpec(ModeSpecContext ctx) {
IdContext idContext = ctx.id();
if (idContext == null || idContext.start == null) {
return;
}
if (idContext.start != null) {
lexerModes.add(idContext.start);
lexerModeNames.add(idContext.start.getText());
List<Token> references = possibleLexerModes.remove(idContext.start.getText());
if (references != null) {
lexerModes.addAll(references);
}
}
}
@Override
@RuleDependencies({
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerCommand, version=1, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerCommandName, version=1, dependents=Dependents.DESCENDANTS),
})
public void enterLexerCommand(LexerCommandContext ctx) {
LexerCommandNameContext lexerCommandNameContext = ctx.lexerCommandName();
if (lexerCommandNameContext == null || lexerCommandNameContext.start == null) {
return;
}
if (lexerCommandNameContext.start != null) {
lexerCommands.add(lexerCommandNameContext.start);
if ("pushMode".equals(lexerCommandNameContext.start.getText()) || "mode".equals(lexerCommandNameContext.start.getText())) {
inNamedModeCommand++;
}
}
}
@Override
@RuleDependencies({
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerCommand, version=1, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerCommandName, version=1, dependents=Dependents.DESCENDANTS),
})
public void exitLexerCommand(LexerCommandContext ctx) {
LexerCommandNameContext lexerCommandNameContext = ctx.lexerCommandName();
if (lexerCommandNameContext != null && lexerCommandNameContext.start != null) {
if ("pushMode".equals(lexerCommandNameContext.start.getText()) || "mode".equals(lexerCommandNameContext.start.getText())) {
inNamedModeCommand--;
}
}
}
@Override
@RuleDependencies({
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerCommandExpr, version=1, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_id, version=1, dependents=Dependents.DESCENDANTS),
})
public void enterLexerCommandExpr(LexerCommandExprContext ctx) {
if (inNamedModeCommand > 0) {
IdContext idContext = ctx.id();
if (idContext != null && idContext.start != null) {
if (lexerModeNames.contains(idContext.start.getText())) {
lexerModes.add(idContext.start);
} else {
List<Token> list = possibleLexerModes.get(idContext.start.getText());
if (list == null) {
list = new ArrayList<>(1);
possibleLexerModes.put(idContext.start.getText(), list);
}
list.add(idContext.start);
}
}
}
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_block, version=0, dependents=Dependents.PARENTS)
public void enterBlock(BlockContext ctx) {
blockLevel++;
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_block, version=0, dependents=Dependents.PARENTS)
public void exitBlock(BlockContext ctx) {
blockLevel--;
}
@Override
@RuleDependencies({
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_option, version=6, dependents=Dependents.ANCESTORS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_id, version=1, dependents=Dependents.DESCENDANTS),
})
public void enterOption(OptionContext ctx) {
IdContext id = ctx.id();
if (id != null) {
String option = id.start.getText();
boolean valid;
if (blockLevel > 0) {
valid = knownBlockOptions.contains(option);
} else if (ruleLevel > 0) {
valid = knownRuleOptions.contains(option);
} else {
switch (grammarType) {
case GrammarParser.LEXER:
valid = knownLexerGrammarOptions.contains(option);
break;
case GrammarParser.PARSER:
valid = knownParserGrammarOptions.contains(option);
break;
case GrammarParser.COMBINED:
default:
valid = knownCombinedGrammarOptions.contains(option);
break;
}
}
if (!valid) {
invalidOptions.add(id.start);
}
}
}
@Override
@RuleDependencies({
// The first two rules together form the dependency on constructs which an element option
// can be defined on.
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_elementOption, version=3, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_elementOptions, version=5, dependents=Dependents.PARENTS),
// This is for the actual text of the option
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_id, version=1, dependents=Dependents.DESCENDANTS),
// These are for the specific contexts an element option applies to
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_terminal, version=0, dependents=Dependents.SELF),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_setElement, version=4, dependents=Dependents.SELF),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_atom, version=0, dependents=Dependents.SELF),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerAtom, version=1, dependents=Dependents.SELF),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_alternative, version=5, dependents=Dependents.SELF),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleref, version=5, dependents=Dependents.SELF),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_element, version=5, dependents=Dependents.SELF),
})
public void enterElementOption(ElementOptionContext ctx) {
IdContext id = ctx.id();
if (id == null) {
return;
}
ElementOptionsContext elementOptions = (ElementOptionsContext)ctx.parent;
if (elementOptions == null || elementOptions.parent == null) {
// only mark as invalid if we are sure
// TODO: is this the desired behavior?
return;
}
Set<String> knownOptions;
switch (elementOptions.parent.getRuleIndex()) {
case GrammarParser.RULE_terminal:
knownOptions = KnownTerminalOptions;
break;
case GrammarParser.RULE_setElement:
knownOptions = KnownSetElementOptions;
break;
case GrammarParser.RULE_atom:
knownOptions = ((AtomContext)elementOptions.parent).DOT() != null ? KnownAtomOptions : null;
break;
case GrammarParser.RULE_lexerAtom:
knownOptions = ((LexerAtomContext)elementOptions.parent).DOT() != null ? KnownLexerAtomOptions : null;
break;
case GrammarParser.RULE_alternative:
knownOptions = KnownAlternativeOptions;
break;
case GrammarParser.RULE_ruleref:
knownOptions = KnownRuleReferenceOptions;
break;
case GrammarParser.RULE_element:
knownOptions = ((ElementContext)elementOptions.parent).QUESTION() != null ? KnownSemanticPredicateOptions : KnownEmbeddedActionOptions;
break;
default:
knownOptions = null;
break;
}
if (knownOptions == null) {
return;
}
String option = id.start.getText();
boolean valid = knownOptions.contains(option);
if (!valid) {
invalidOptions.add(id.start);
}
}
}
}