/*
* 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.syndiag;
import java.awt.Component;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Properties;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import org.antlr.netbeans.editor.text.DocumentSnapshot;
import org.antlr.netbeans.editor.text.OffsetRegion;
import org.antlr.netbeans.editor.text.SnapshotPositionRegion;
import org.antlr.netbeans.parsing.spi.ParserData;
import org.antlr.netbeans.parsing.spi.ParserDataEvent;
import org.antlr.netbeans.parsing.spi.ParserDataListener;
import org.antlr.netbeans.parsing.spi.ParserTaskManager;
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.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.antlr.v4.tool.Grammar;
import org.antlr.works.editor.grammar.GrammarParserDataDefinitions;
import org.antlr.works.editor.grammar.codemodel.FileModel;
import org.antlr.works.editor.grammar.codemodel.TokenData;
import org.antlr.works.editor.grammar.experimental.CurrentRuleContextData;
import org.antlr.works.editor.grammar.experimental.GrammarParser;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.AltListContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.AlternativeContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.AtomContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.EbnfSuffixContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerAltContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerAltListContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerAtomContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerBlockContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.LexerRuleContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.ParserRuleSpecContext;
import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.RuleAltListContext;
import org.antlr.works.editor.grammar.experimental.generated.GrammarParserBaseListener;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.api.settings.ConvertAsProperties;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.Parameters;
import org.openide.windows.TopComponent;
import org.openide.windows.WindowManager;
/**
* Top component which displays something.
*
* @author Sam Harwell
*/
@ConvertAsProperties(dtd = "-//org.antlr.works.editor.grammar.syndiag//SyntaxDiagram//EN",
autostore = false)
@TopComponent.Description(preferredID = "SyntaxDiagramTopComponent",
//iconBase="SET/PATH/TO/ICON/HERE",
persistenceType = TopComponent.PERSISTENCE_ALWAYS)
@TopComponent.Registration(mode = "bottomSlidingSide", openAtStartup = true)
@ActionID(category = "Window", id = "org.antlr.works.editor.grammar.syndiag.SyntaxDiagramTopComponent")
@ActionReference(path = "Menu/Window" /*
* , position = 333
*/)
@TopComponent.OpenActionRegistration(displayName = "#CTL_SyntaxDiagramAction",
preferredID = "SyntaxDiagramTopComponent")
@NbBundle.Messages({
"CTL_SyntaxDiagramAction=Syntax Diagram",
"CTL_SyntaxDiagramTopComponent=Syntax",
"HINT_SyntaxDiagramTopComponent=Grammar Rule Syntax Diagram"
})
public final class SyntaxDiagramTopComponent extends TopComponent {
private WeakReference<DocumentSnapshot> snapshot;
private WeakReference<GrammarParser.RuleSpecContext> context;
private Diagram diagram;
public SyntaxDiagramTopComponent() {
initComponents();
setName(Bundle.CTL_SyntaxDiagramTopComponent());
setToolTipText(Bundle.HINT_SyntaxDiagramTopComponent());
ParserTaskManager taskManager = Lookup.getDefault().lookup(ParserTaskManager.class);
taskManager.addDataListener(GrammarParserDataDefinitions.CURRENT_RULE_CONTEXT, new CurrentRuleContextListener());
}
public static SyntaxDiagramTopComponent getInstance() {
return (SyntaxDiagramTopComponent)WindowManager.getDefault().findTopComponent("SyntaxDiagramTopComponent");
}
@CheckForNull
public DocumentSnapshot getSnapshot() {
WeakReference<DocumentSnapshot> ref = snapshot;
if (ref == null) {
return null;
}
return ref.get();
}
@CheckForNull
public GrammarParser.RuleSpecContext getContext() {
WeakReference<GrammarParser.RuleSpecContext> ref = context;
if (ref == null) {
return null;
}
return ref.get();
}
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleSpec, version=0, dependents=Dependents.SELF)
public void setRuleContext(@NullAllowed CurrentRuleContextData context) {
if (!SwingUtilities.isEventDispatchThread()) {
throw new UnsupportedOperationException();
}
if (context == null) {
clearDiagram();
return;
}
DocumentSnapshot currentSnapshot = getSnapshot();
GrammarParser.RuleSpecContext currentRuleSpecContext = getContext();
GrammarParser.RuleSpecContext ruleSpecContext = context.getContext();
if (isSameSnapshot(getSnapshot(), context.getSnapshot()) && isSameContext(getContext(), ruleSpecContext)) {
return;
}
if (diagram != null) {
this.jScrollPane1.setViewportView(null);
diagram = null;
}
this.snapshot = new WeakReference<>(context.getSnapshot());
this.context = new WeakReference<>(ruleSpecContext);
if (ruleSpecContext != null) {
try {
SyntaxBuilderListener listener = new SyntaxBuilderListener(context.getGrammarType(), context.getSnapshot(), context.getFileModel());
new ParseTreeWalker().walk(listener, ruleSpecContext);
this.diagram = new Diagram(listener.getRule());
this.jScrollPane1.setViewportView(diagram);
this.jScrollPane1.validate();
this.diagram.getRule().updatePositions();
} catch (NullPointerException | IllegalArgumentException ex) {
clearDiagram();
}
}
}
private void clearDiagram() {
if (jScrollPane1 != null) {
this.jScrollPane1.setViewportView(null);
}
this.diagram = null;
}
private static boolean isSameSnapshot(DocumentSnapshot a, DocumentSnapshot b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
return a.equals(b);
}
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleSpec, version=0, dependents=Dependents.SELF)
private static boolean isSameContext(GrammarParser.RuleSpecContext a, GrammarParser.RuleSpecContext b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
return isSameToken(a.start, b.start) && isSameToken(a.stop, b.stop);
}
private static boolean isSameToken(Token a, Token b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
return a.getStartIndex() == b.getStartIndex()
&& a.getStopIndex() == b.getStopIndex();
}
/**
* This method is called from within the constructor to
* initialize the form.
* WARNING: Do NOT modify this code. The content of this method is
* always regenerated by the Form Editor.
*/
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
jScrollPane1 = new javax.swing.JScrollPane();
setLayout(new java.awt.BorderLayout());
jScrollPane1.setBackground(new java.awt.Color(255, 255, 255));
add(jScrollPane1, java.awt.BorderLayout.CENTER);
}// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JScrollPane jScrollPane1;
// End of variables declaration//GEN-END:variables
@Override
public void componentOpened() {
// TODO add custom code on component opening
}
@Override
public void componentClosed() {
// TODO add custom code on component closing
}
void writeProperties(Properties properties) {
// better to version settings since initial version as advocated at
// http://wiki.apidesign.org/wiki/PropertyFiles
properties.setProperty("version", "1.0");
// TODO store your settings
}
void readProperties(Properties properties) {
String version = properties.getProperty("version");
// TODO read your settings according to their version
}
private class CurrentRuleContextListener implements ParserDataListener<CurrentRuleContextData> {
@Override
public void dataChanged(ParserDataEvent<? extends CurrentRuleContextData> event) {
ParserData<? extends CurrentRuleContextData> parserData = event.getData();
final CurrentRuleContextData RuleSpecContext = parserData.getData();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
SyntaxDiagramTopComponent syntaxDiagram = SyntaxDiagramTopComponent.getInstance();
if (syntaxDiagram != null) {
syntaxDiagram.setRuleContext(RuleSpecContext);
}
}
});
}
}
private static class SyntaxBuilderListener extends GrammarParserBaseListener {
private final int grammarType;
private final DocumentSnapshot snapshot;
private final FileModel fileModel;
private final Deque<JComponent> nodes = new ArrayDeque<>();
private Rule RuleSpec;
private ParserRuleContext outermostAtom;
public SyntaxBuilderListener(int grammarType, DocumentSnapshot snapshot, FileModel fileModel) {
Parameters.notNull("snapshot", snapshot);
this.grammarType = grammarType;
this.snapshot = snapshot;
this.fileModel = fileModel;
}
public Rule getRule() {
return RuleSpec;
}
/*
* RuleSpec is the top level context
*/
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.PARENTS)
public void enterParserRuleSpec(ParserRuleSpecContext ctx) {
@SuppressWarnings("LocalVariableHidesMemberVariable")
Rule RuleSpec = new Rule(ctx.name.getText());
this.RuleSpec = RuleSpec;
nodes.push(RuleSpec);
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.PARENTS)
public void exitParserRuleSpec(ParserRuleSpecContext ctx) {
assert nodes.size() == 1;
this.RuleSpec = (Rule)nodes.pop();
// this.RuleSpec.setupElements();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerRule, version=0, dependents=Dependents.PARENTS)
public void enterLexerRule(LexerRuleContext ctx) {
@SuppressWarnings("LocalVariableHidesMemberVariable")
Rule RuleSpec = new Rule(ctx.name.getText());
this.RuleSpec = RuleSpec;
nodes.push(RuleSpec);
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerRule, version=0, dependents=Dependents.PARENTS)
public void exitLexerRule(LexerRuleContext ctx) {
assert nodes.size() == 1;
this.RuleSpec = (Rule)nodes.pop();
// this.RuleSpec.setupElements();
}
/*
* RuleSpecAltList and altList form the true body of a Block
*/
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleAltList, version=0, dependents=Dependents.PARENTS)
public void enterRuleAltList(RuleAltListContext ctx) {
enterBlock();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_altList, version=0, dependents=Dependents.PARENTS)
public void enterAltList(AltListContext ctx) {
enterBlock();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerAltList, version=1, dependents=Dependents.PARENTS)
public void enterLexerAltList(LexerAltListContext ctx) {
enterBlock();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerBlock, version=1, dependents=Dependents.PARENTS)
public void enterLexerBlock(LexerBlockContext ctx) {
enterBlock();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleAltList, version=0, dependents=Dependents.PARENTS)
public void exitRuleAltList(RuleAltListContext ctx) {
exitBlock();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_altList, version=0, dependents=Dependents.PARENTS)
public void exitAltList(AltListContext ctx) {
exitBlock();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerAltList, version=1, dependents=Dependents.PARENTS)
public void exitLexerAltList(LexerAltListContext ctx) {
exitBlock();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerBlock, version=1, dependents=Dependents.PARENTS)
public void exitLexerBlock(LexerBlockContext ctx) {
exitBlock();
}
/*
* alternative (parser) and lexerAlt (lexer) are fairly straightforward
*/
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_alternative, version=5, dependents=Dependents.PARENTS)
public void enterAlternative(AlternativeContext ctx) {
enterAlternative();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerAlt, version=3, dependents=Dependents.PARENTS)
public void enterLexerAlt(LexerAltContext ctx) {
enterAlternative();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_alternative, version=5, dependents=Dependents.PARENTS)
public void exitAlternative(AlternativeContext ctx) {
exitAlternative();
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerAlt, version=3, dependents=Dependents.PARENTS)
public void exitLexerAlt(LexerAltContext ctx) {
exitAlternative();
}
/*
* TODO: handle special actions and label sections similarly (was rewrites)
*/
/*
* Actual elements (atoms only for a test)
*/
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerAtom, version=1, dependents=Dependents.PARENTS)
public void enterLexerAtom(LexerAtomContext ctx) {
enterEveryAtom(ctx);
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_atom, version=5, dependents=Dependents.PARENTS)
public void enterAtom(AtomContext ctx) {
enterEveryAtom(ctx);
}
@RuleDependencies({
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_terminal, version=1, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleref, version=5, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_range, version=4, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_notSet, version=1, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_blockSet, version=0, dependents=Dependents.PARENTS),
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_setElement, version=4, dependents=Dependents.PARENTS),
})
public void enterEveryAtom(ParserRuleContext ctx) {
if (outermostAtom != null) {
return;
}
outermostAtom = ctx;
SnapshotPositionRegion sourceSpan = null;
if (ctx.stop != null) {
sourceSpan = new SnapshotPositionRegion(snapshot, OffsetRegion.fromBounds(ctx.start.getStartIndex(), ctx.stop.getStopIndex() + 1));
} else if (ctx.start != null) {
sourceSpan = new SnapshotPositionRegion(snapshot, OffsetRegion.fromBounds(ctx.start.getStartIndex(), ctx.start.getStopIndex() + 1));
}
boolean wildcard = ctx.start.getType() == GrammarParser.DOT;
boolean hasChild = ctx.children != null && !ctx.children.isEmpty();
boolean reference = hasChild
&& (ctx.children.get(0) instanceof GrammarParser.TerminalContext
|| ctx.children.get(0) instanceof GrammarParser.RulerefContext);
boolean range = hasChild && ctx.children.get(0) instanceof GrammarParser.RangeContext;
boolean notset = hasChild && ctx.children.get(0) instanceof GrammarParser.NotSetContext;
boolean charSet = hasChild
&& ctx.children.get(0) instanceof TerminalNode
&& ((TerminalNode)ctx.children.get(0)).getSymbol().getType() == GrammarParser.LEXER_CHAR_SET;
if (wildcard || reference) {
String text = ctx.start.getText();
boolean nonTerminal = !(Grammar.isTokenName(text) || text.startsWith("'"));
if (!nonTerminal && Grammar.isTokenName(text) && RuleSpec != null && Grammar.isTokenName(RuleSpec.getRuleName())) {
nonTerminal = true;
}
if (nonTerminal) {
nodes.peek().add(new NonTerminal(text, sourceSpan));
} else {
if (Grammar.isTokenName(text)) {
String literal = null;
for (TokenData tokenData : fileModel.getVocabulary().getTokens()) {
if (tokenData.getLiteral() != null && text.equals(tokenData.getName())) {
if (literal != null) {
// multiple matches
literal = null;
break;
}
literal = tokenData.getLiteral();
}
}
if (literal != null) {
text = literal;
}
}
nodes.peek().add(new Terminal(text, sourceSpan));
}
} else if (range) {
String label = "???";
Terminal terminal = null;
GrammarParser.RangeContext rangeContext = (GrammarParser.RangeContext)ctx.children.get(0);
if (rangeContext.children != null && rangeContext.children.size() == 3) {
Token start = ((TerminalNode)rangeContext.children.get(0)).getSymbol();
Token end = ((TerminalNode)rangeContext.children.get(2)).getSymbol();
if (start != null && end != null) {
terminal = new RangeTerminal(start.getText(), end.getText(), sourceSpan);
}
}
if (terminal == null) {
terminal = new Terminal(label, sourceSpan);
}
nodes.peek().add(terminal);
} else if (notset) {
GrammarParser.NotSetContext notSetContext = (GrammarParser.NotSetContext)ctx.children.get(0);
List<GrammarParser.SetElementContext> elementContexts;
if (notSetContext.setElement() != null) {
elementContexts = Collections.singletonList(notSetContext.setElement());
} else {
elementContexts = new ArrayList<>(notSetContext.blockSet().setElement());
}
nodes.peek().add(new SetTerminal(elementContexts, sourceSpan, true));
} else if (charSet) {
nodes.peek().add(new SetTerminal((TerminalNode)ctx.children.get(0), sourceSpan));
} else {
nodes.peek().add(new Terminal("???", sourceSpan));
}
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerAtom, version=1, dependents=Dependents.PARENTS)
public void exitLexerAtom(LexerAtomContext ctx) {
if (outermostAtom == ctx) {
outermostAtom = null;
}
}
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_atom, version=5, dependents=Dependents.PARENTS)
public void exitAtom(AtomContext ctx) {
if (outermostAtom == ctx) {
outermostAtom = null;
}
}
/*
* suffix
*/
@Override
@RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ebnfSuffix, version=5, dependents=Dependents.ANCESTORS)
public void enterEbnfSuffix(EbnfSuffixContext ctx) {
Block block;
boolean greedy = ctx.getChildCount() <= 1;
switch (ctx.start.getType()) {
case GrammarParser.QUESTION:
{
block = new Block();
JComponent last = nodes.peek();
Component lastChild = last.getComponent(last.getComponentCount() - 1);
last.remove(last.getComponentCount() - 1);
Alt alt = new Alt();
alt.add(lastChild);
if (greedy) {
block.add(alt);
block.add(new Alt());
} else {
block.add(new Alt());
block.add(alt);
}
last.add(block);
break;
}
case GrammarParser.STAR:
case GrammarParser.PLUS:
{
block = new PlusBlock(greedy);
JComponent last = nodes.peek();
Component lastChild = last.getComponent(last.getComponentCount() - 1);
last.remove(last.getComponentCount() - 1);
Alt alt = new Alt();
alt.add(lastChild);
block.add(alt);
if (ctx.start.getType() == GrammarParser.STAR) {
Block optionalBlock = new Block();
alt = new Alt();
alt.add(block);
if (greedy) {
optionalBlock.add(alt);
optionalBlock.add(new Alt());
} else {
optionalBlock.add(new Alt());
optionalBlock.add(alt);
}
block = optionalBlock;
}
last.add(block);
break;
}
default:
break;
}
}
/*
* helper methods
*/
private void enterBlock() {
if (outermostAtom != null) {
return;
}
Block block = new Block();
nodes.push(block);
}
private void exitBlock() {
if (outermostAtom != null) {
return;
}
Component block = nodes.pop();
nodes.peek().add(block);
}
private void enterAlternative() {
if (outermostAtom != null) {
return;
}
Alt alternative = new Alt();
nodes.push(alternative);
}
private void exitAlternative() {
if (outermostAtom != null) {
return;
}
Component alternative = nodes.pop();
nodes.peek().add(alternative);
}
}
}