/* * 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.refactoring; import java.awt.Component; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Icon; import javax.swing.text.BadLocationException; import javax.swing.text.JTextComponent; import org.antlr.netbeans.editor.navigation.Description; import org.antlr.netbeans.editor.text.DocumentSnapshot; import org.antlr.netbeans.editor.text.DocumentSnapshotLine; import org.antlr.netbeans.editor.text.OffsetRegion; import org.antlr.netbeans.editor.text.SnapshotPosition; import org.antlr.netbeans.editor.text.SnapshotPositionRegion; import org.antlr.netbeans.editor.text.TrackingPosition; import org.antlr.netbeans.editor.text.TrackingPositionRegion; import org.antlr.netbeans.editor.text.VersionedDocument; import org.antlr.netbeans.editor.text.VersionedDocumentUtilities; import org.antlr.netbeans.parsing.spi.ParserData; import org.antlr.netbeans.parsing.spi.ParserDataOptions; import org.antlr.netbeans.parsing.spi.ParserTaskManager; import org.antlr.v4.runtime.Dependents; import org.antlr.v4.runtime.RuleDependencies; import org.antlr.v4.runtime.RuleDependency; import org.antlr.v4.runtime.Token; import org.antlr.v4.runtime.misc.Interval; import org.antlr.v4.runtime.misc.Tuple; import org.antlr.v4.runtime.misc.Tuple3; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; import org.antlr.v4.tool.Grammar; import org.antlr.works.editor.antlr4.parsing.ParseTrees; import org.antlr.works.editor.grammar.GoToSupport; import org.antlr.works.editor.grammar.GrammarDataObject; import org.antlr.works.editor.grammar.GrammarEditorKit; import org.antlr.works.editor.grammar.GrammarParserDataDefinitions; import org.antlr.works.editor.grammar.completion.GrammarCompletionProvider; import org.antlr.works.editor.grammar.experimental.GrammarParser; import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.ActionBlockContext; import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.GrammarSpecContext; 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.RuleActionContext; import org.antlr.works.editor.grammar.experimental.generated.AbstractGrammarParser.RulePrequelsContext; import org.antlr.works.editor.grammar.experimental.generated.GrammarParserBaseVisitor; import org.netbeans.api.annotations.common.CheckForNull; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.editor.EditorRegistry; import org.netbeans.editor.BaseDocument; import org.netbeans.editor.GuardedDocument; import org.netbeans.modules.editor.indent.api.Indent; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.awt.ActionRegistration; import org.openide.cookies.EditorCookie; import org.openide.text.NbDocument; import org.openide.util.ContextAwareAction; import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.NbBundle.Messages; @ActionID( category = "Refactoring", id = "org.antlr.works.editor.grammar.refactoring.IncrementRuleVersionAction") @ActionRegistration( displayName = "#CTL_IncrementRuleVersionAction", lazy = false) @ActionReference(path = "Editors/" + GrammarEditorKit.GRAMMAR_MIME_TYPE + "/Popup/refactor", position = 100) @Messages("CTL_IncrementRuleVersionAction=Increment Rule Version") public final class IncrementRuleVersionAction extends AbstractAction implements ContextAwareAction { // -J-Dorg.antlr.works.editor.grammar.refactoring.IncrementRuleVersionAction.level=FINE private static final Logger LOGGER = Logger.getLogger(IncrementRuleVersionAction.class.getName()); @NullAllowed private final Lookup _context; private final GrammarDataObject _dataObject; private final EditorCookie _editorCookie; private final DocumentSnapshot _snapshot; private final Description _description; public IncrementRuleVersionAction() { this(null); } public IncrementRuleVersionAction(@NullAllowed Lookup context) { super(Bundle.CTL_IncrementRuleVersionAction()); this._context = context; this._dataObject = context != null ? context.lookup(GrammarDataObject.class) : null; this._editorCookie = context != null ? context.lookup(EditorCookie.class) : null; if (_editorCookie != null) { JTextComponent focused = EditorRegistry.focusedComponent(); if (!_editorCookie.getDocument().equals(focused.getDocument())) { focused = _editorCookie.getOpenedPanes()[0]; } Token token = GoToSupport.getContext(_editorCookie.getDocument(), focused.getCaretPosition()); if (token != null && token.getType() == GrammarParser.RULE_REF) { ParserTaskManager parserTaskManager = Lookup.getDefault().lookup(ParserTaskManager.class); VersionedDocument versionedDocument = VersionedDocumentUtilities.getVersionedDocument(_editorCookie.getDocument()); _snapshot = versionedDocument.getCurrentSnapshot(); Collection<Description> rules = GrammarCompletionProvider.getRulesFromGrammar(parserTaskManager, _snapshot, false); SnapshotPosition caretPosition = new SnapshotPosition(_snapshot, focused.getCaretPosition()); Description currentDescription = null; for (Description description : rules) { if (Grammar.isTokenName(description.getName())) { continue; } if (!_dataObject.getPrimaryFile().equals(description.getFileObject())) { continue; } SnapshotPositionRegion namePosition = new SnapshotPositionRegion(_snapshot, description.getOffset(), description.getName().length()); if (caretPosition.compareTo(namePosition.getStart()) >= 0 && caretPosition.compareTo(namePosition.getEnd()) <= 0) { currentDescription = description; break; } } _description = currentDescription; } else { _snapshot = null; _description = null; } } else { _snapshot = null; _description = null; } } @Override public boolean isEnabled() { return _description != null; } @Override public Action createContextAwareInstance(Lookup actionContext) { if (actionContext == null) { return null; } if (actionContext.lookup(GrammarDataObject.class) == null) { return null; } if (actionContext.lookup(EditorCookie.class) == null) { return null; } return new IncrementRuleVersionAction(actionContext); } @Override @RuleDependencies({ @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_grammarSpec, version=0, dependents=Dependents.SELF), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.SELF), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleAction, version=0, dependents=Dependents.SELF), }) public void actionPerformed(ActionEvent ev) { try { ParserTaskManager parserTaskManager = Lookup.getDefault().lookup(ParserTaskManager.class); // check for a rule at the caret position Future<ParserData<GrammarSpecContext>> futureData = parserTaskManager.getData(_snapshot, GrammarParserDataDefinitions.REFERENCE_PARSE_TREE, EnumSet.of(ParserDataOptions.SYNCHRONOUS)); ParserData<GrammarSpecContext> data = futureData != null ? futureData.get() : null; GrammarSpecContext grammarSpecContext = data != null ? data.getData() : null; if (grammarSpecContext == null) { return; } Map<ParserRuleSpecContext, String> rules = getRules(grammarSpecContext); Map<ParserRuleSpecContext, Tuple3<RuleActionContext, TerminalNode, Integer>> versionedRules = getVersionedRules(rules); int maximumVersion = getMaximumVersionNumber(versionedRules); ParserRuleSpecContext currentRule = findRuleForDescription(rules, _description); if (currentRule == null) { throw new UnsupportedOperationException("Could not locate the current rule in the parse tree."); // NotificationDisplayer.getDefault().notify("Could Not Apply 'Increment Rule Version'", EmptyIcon.INSTANCE, message, (ActionListener)null, Priority.NORMAL); // return; } Tuple3<RuleActionContext, TerminalNode, Integer> currentVersion = versionedRules.get(currentRule); if (currentVersion == null) { addVersionNumber(currentRule, maximumVersion + 1); } else if (currentVersion.getItem2() == null) { addVersionNumberToAction(currentVersion.getItem1(), maximumVersion + 1); } else { updateVersionNumber(currentVersion.getItem2(), maximumVersion + 1); } } catch (InterruptedException | ExecutionException ex) { Exceptions.printStackTrace(ex); } } private void updateVersionNumber(TerminalNode currentVersionToken, final int newVersion) { Interval sourceInterval = ParseTrees.getSourceInterval(currentVersionToken); OffsetRegion region = OffsetRegion.fromBounds(sourceInterval.a, sourceInterval.b + 1); TrackingPositionRegion trackingRegion = _snapshot.createTrackingRegion(region, TrackingPositionRegion.Bias.Forward); final SnapshotPositionRegion currentRegion = trackingRegion.getRegion(_snapshot.getVersionedDocument().getCurrentSnapshot()); final BaseDocument baseDocument = (BaseDocument)_snapshot.getVersionedDocument().getDocument(); if (baseDocument == null) { throw new UnsupportedOperationException("No document available"); } baseDocument.runAtomicAsUser(new Runnable() { @Override public void run() { try { baseDocument.remove(currentRegion.getStart().getOffset(), currentRegion.getLength()); baseDocument.insertString(currentRegion.getStart().getOffset(), Integer.toString(newVersion), null); } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } } }); } private void addVersionNumberToAction(RuleActionContext versionAction, int version) { ActionBlockContext actionBlockContext = versionAction.actionBlock(); if (actionBlockContext == null || actionBlockContext.BEGIN_ACTION() == null || actionBlockContext.END_ACTION() == null) { throw new UnsupportedOperationException("Incomplete or invalid action block"); } final String text = Integer.toString(version) + (actionBlockContext.getChildCount() == 2 ? "" : " "); int offset = ParseTrees.getSourceInterval(actionBlockContext.BEGIN_ACTION()).b + 1; TrackingPosition trackingPosition = _snapshot.createTrackingPosition(offset, TrackingPosition.Bias.Forward); final SnapshotPosition currentPosition = trackingPosition.getPosition(_snapshot.getVersionedDocument().getCurrentSnapshot()); final BaseDocument baseDocument = (BaseDocument)_snapshot.getVersionedDocument().getDocument(); if (baseDocument == null) { throw new UnsupportedOperationException("No document available"); } baseDocument.runAtomicAsUser(new Runnable() { @Override public void run() { try { baseDocument.insertString(currentPosition.getOffset(), text, null); } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } } }); } private void addVersionNumber(@NonNull ParserRuleSpecContext ruleContext, int version) { TerminalNode colon = ruleContext.COLON(); if (colon == null) { throw new UnsupportedOperationException("Incomplete rule"); } DocumentSnapshotLine line = _snapshot.findLineFromLineNumber(colon.getSymbol().getLine() - 1); final boolean hasLeadingNewline = line.getText().substring(0, colon.getSymbol().getCharPositionInLine()).trim().isEmpty(); final String text = String.format("%s@version{%d}\n", hasLeadingNewline ? "" : "\n", version); int offset = ParseTrees.getSourceInterval(colon).a; TrackingPosition trackingPosition = _snapshot.createTrackingPosition(offset, TrackingPosition.Bias.Forward); final SnapshotPosition currentPosition = trackingPosition.getPosition(_snapshot.getVersionedDocument().getCurrentSnapshot()); final GuardedDocument document = (GuardedDocument)_snapshot.getVersionedDocument().getDocument(); if (document == null) { throw new UnsupportedOperationException("No " + GuardedDocument.class.getName() + " available"); } Indent indent = Indent.get(document); indent.lock(); try { document.runAtomicAsUser(new Runnable() { @Override public void run() { try { document.insertString(currentPosition.getOffset(), text, null); // reindent the line containing the @version{} action and the (following) line containing the ':' int startLine = currentPosition.getContainingLine().getLineNumber() + (hasLeadingNewline ? 0 : 1); int endLine = startLine + 1; // currently the reindent algorithm only supports operating on one line at a time for (int i = startLine; i <= endLine; i++) { int lineOffset = NbDocument.findLineOffset(document, i); Indent.get(document).reindent(lineOffset); } } catch (BadLocationException ex) { Exceptions.printStackTrace(ex); } } }); } finally { indent.unlock(); } } @RuleDependencies({ @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_grammarSpec, version=0, dependents=Dependents.SELF), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=3, dependents=Dependents.ANCESTORS), }) private Map<ParserRuleSpecContext, String> getRules(GrammarSpecContext grammarSpec) { return RulesVisitor.INSTANCE.visit(grammarSpec); } @RuleDependencies({ @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.SELF), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleAction, version=0, dependents=Dependents.SELF), }) private Map<ParserRuleSpecContext, Tuple3<RuleActionContext, TerminalNode, Integer>> getVersionedRules(Map<ParserRuleSpecContext, String> rules) { Map<ParserRuleSpecContext, Tuple3<RuleActionContext, TerminalNode, Integer>> result = new HashMap<>(); for (Map.Entry<ParserRuleSpecContext, String> entry : rules.entrySet()) { RuleActionContext versionAction = VersionActionVisitor.INSTANCE.visit(entry.getKey()); if (versionAction == null) { continue; } TerminalNode word = null; Integer version = null; List<TerminalNode> words = findActionWords(versionAction.actionBlock()); if (words.size() != 1) { LOGGER.log(Level.WARNING, "{0}", String.format("The '@version{}' block for rule '%s' should only contain a single non-negative integer.", entry.getValue())); } for (TerminalNode node : words) { version = parseVersion(node); if (version != null) { word = node; break; } } result.put(entry.getKey(), Tuple.create(versionAction, word, version)); } return result; } @RuleDependencies({ @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_actionBlock, version=5, dependents={Dependents.PARENTS, Dependents.DESCENDANTS}), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleAction, version=0, dependents=Dependents.SELF), }) private List<TerminalNode> findActionWords(ActionBlockContext ctx) { if (!(ctx.getParent() instanceof RuleActionContext)) { LOGGER.log(Level.WARNING, "Only expected to analyze 'actionBlock' nodes in a 'ruleAction' context."); return Collections.emptyList(); } RuleActionContext ruleActionContext = (RuleActionContext)ctx.getParent(); if (ruleActionContext.id() == null || !"version".equals(ruleActionContext.id().getText())) { LOGGER.log(Level.WARNING, "Only expected to analyze 'actionBlock' nodes for a @version{} action."); return Collections.emptyList(); } boolean reportedProblem = false; List<TerminalNode> result = new ArrayList<>(); for (int i = 0; i < ctx.getChildCount(); i++) { String problem = null; ParseTree child = ctx.getChild(i); if (!(child instanceof TerminalNode)) { problem = "The '@version{}' block for a rule should only contain a single non-negative integer."; } if (problem == null) { int symbolType = ((TerminalNode)child).getSymbol().getType(); if (i == 0) { if (symbolType != GrammarParser.BEGIN_ACTION) { problem = "The 'actionBlock' rule should start with a 'BEGIN_ACTION' terminal."; } } else if (i == ctx.getChildCount() - 1) { if (symbolType != GrammarParser.END_ACTION) { problem = "The 'actionBlock' rule should end with an 'END_ACTION' terminal."; } } else { switch (symbolType) { case GrammarParser.BEGIN_ACTION: problem = "Unexpected 'BEGIN_ACTION' terminal after start of 'actionBlock' rule."; break; case GrammarParser.END_ACTION: problem = "Unexpected 'END_ACTION' terminal before end of 'actionBlock' rule."; break; case GrammarParser.ACTION_WS: case GrammarParser.ACTION_NEWLINE: case GrammarParser.ACTION_COMMENT: // ignore these tokens continue; case GrammarParser.ACTION_WORD: // this is the token we're interested in result.add((TerminalNode)child); continue; default: // anything is a problem problem = "The '@version{}' block for a rule should only contain a single non-negative integer."; break; } } } if (!reportedProblem && problem != null) { String file = _description.getFileObject().getNameExt(); int line = 0; int column = 0; LOGGER.log(Level.WARNING, "{0}:{1}:{2}: {3}", new Object[] { file, line, column, problem }); reportedProblem = true; } } return result; } @CheckForNull private static Integer parseVersion(@NonNull TerminalNode node) { try { int result = Integer.parseInt(node.getText()); if (result >= 0) { // success return result; } return null; } catch (NumberFormatException ex) { return null; } } @RuleDependencies({ // doesn't use any of the rule context inputs }) private static int getMaximumVersionNumber(@NonNull Map<ParserRuleSpecContext, Tuple3<RuleActionContext, TerminalNode, Integer>> versionedRules) { int maxVersion = 0; for (Map.Entry<ParserRuleSpecContext, Tuple3<RuleActionContext, TerminalNode, Integer>> entry : versionedRules.entrySet()) { Integer ruleVersion = entry.getValue().getItem3(); if (ruleVersion != null) { maxVersion = Math.max(maxVersion, ruleVersion); } } return maxVersion; } @CheckForNull @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.SELF) private static ParserRuleSpecContext findRuleForDescription(@NonNull Map<ParserRuleSpecContext, String> rules, @NonNull Description description) { for (Map.Entry<ParserRuleSpecContext, String> entry : rules.entrySet()) { if (!description.getName().equals(entry.getValue())) { continue; } Interval sourceInterval = ParseTrees.getSourceInterval(entry.getKey()); if (sourceInterval.a <= description.getOffset() && sourceInterval.b >= description.getOffset()) { return entry.getKey(); } } return null; } private static final class EmptyIcon implements Icon { public static final EmptyIcon INSTANCE = new EmptyIcon(); @Override public void paintIcon(Component c, Graphics g, int x, int y) { } @Override public int getIconWidth() { return 0; } @Override public int getIconHeight() { return 0; } } @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.SELF) private static final class RulesVisitor extends GrammarParserBaseVisitor<Map<ParserRuleSpecContext, String>> { public static final RulesVisitor INSTANCE = new RulesVisitor(); @Override @RuleDependencies({ @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_grammarSpec, version=0, dependents=Dependents.SELF), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.SELF), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleAction, version=0, dependents=Dependents.SELF), }) @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.SELF) public Map<ParserRuleSpecContext, String> visitParserRuleSpec(ParserRuleSpecContext ctx) { TerminalNode nameToken = ctx.RULE_REF(); String name = nameToken != null ? nameToken.getText() : ""; return Collections.singletonMap(ctx, name); } @Override @RuleDependencies({ @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerRule, version=0, dependents=Dependents.SELF), // for the assumption about which contexts can contain parserRuleSpec: @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=3, dependents=Dependents.ANCESTORS), }) public Map<ParserRuleSpecContext, String> visitLexerRule(LexerRuleContext ctx) { // optimization: lexer rules cannot contain parser rules return defaultResult(); } @Override @RuleDependencies({ // no dependencies }) protected Map<ParserRuleSpecContext, String> aggregateResult(Map<ParserRuleSpecContext, String> aggregate, Map<ParserRuleSpecContext, String> nextResult) { if (aggregate.isEmpty()) { return nextResult; } if (nextResult.isEmpty()) { return aggregate; } Map<ParserRuleSpecContext, String> result = new HashMap<>(aggregate); result.putAll(nextResult); return result; } @Override @RuleDependencies({ // no dependencies }) protected Map<ParserRuleSpecContext, String> defaultResult() { return Collections.emptyMap(); } } @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleAction, version=0, dependents=Dependents.SELF) private static final class VersionActionVisitor extends GrammarParserBaseVisitor<RuleActionContext> { public static final VersionActionVisitor INSTANCE = new VersionActionVisitor(); @Override @RuleDependencies({ @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=0, dependents=Dependents.SELF), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_rulePrequels, version=0, dependents=Dependents.SELF), }) public RuleActionContext visitParserRuleSpec(ParserRuleSpecContext ctx) { RulePrequelsContext rulePrequelsContext = ctx.rulePrequels(); if (rulePrequelsContext == null) { return null; } return visit(rulePrequelsContext); } @Override @RuleDependencies({ @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_lexerRule, version=0, dependents=Dependents.SELF), @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_parserRuleSpec, version=3, dependents=Dependents.ANCESTORS), }) public RuleActionContext visitLexerRule(LexerRuleContext ctx) { // lexer rules aren't versioned (optimization) return null; } @Override @RuleDependency(recognizer=GrammarParser.class, rule=GrammarParser.RULE_ruleAction, version=0, dependents=Dependents.SELF) public RuleActionContext visitRuleAction(RuleActionContext ctx) { if (ctx.id() == null || !"version".equals(ctx.id().getText())) { return null; } return ctx; } @Override @RuleDependencies({ // no dependencies }) protected RuleActionContext aggregateResult(RuleActionContext aggregate, RuleActionContext nextResult) { if (aggregate != null && nextResult != null) { LOGGER.log(Level.WARNING, "Found multiple @version{} actions for rule."); } if (aggregate != null) { return aggregate; } return nextResult; } } }