/*
* 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.semantics;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyledDocument;
import org.antlr.netbeans.editor.classification.TokenTag;
import org.antlr.netbeans.editor.tagging.TaggedPositionRegion;
import org.antlr.netbeans.editor.tagging.Tagger;
import org.antlr.netbeans.editor.text.DocumentSnapshot;
import org.antlr.netbeans.editor.text.NormalizedSnapshotPositionRegionCollection;
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.TrackingPositionRegion;
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.Lexer;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.Tuple;
import org.antlr.v4.runtime.misc.Tuple2;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.TerminalNode;
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.codemodel.FileModel;
import org.antlr.works.editor.grammar.experimental.CurrentRuleContextData;
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.editor.BaseDocument;
import org.netbeans.lib.editor.util.swing.DocumentUtilities;
import org.netbeans.modules.editor.errorstripe.privatespi.Mark;
import org.netbeans.modules.editor.errorstripe.privatespi.MarkProviderCreator;
import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory;
import org.netbeans.spi.editor.highlighting.HighlightsSequence;
import org.netbeans.spi.editor.highlighting.ZOrder;
import org.netbeans.spi.editor.highlighting.support.OffsetsBag;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.WeakListeners;
/**
*
* @author Sam Harwell
*/
@NbBundle.Messages({
"LBL_ERROR_STRIPE_TOOLTIP=Mark Occurrences"
})
public class MarkOccurrencesHighlighter extends AbstractSemanticHighlighter<CurrentRuleContextData> {
// -J-Dorg.antlr.works.editor.grammar.semantics.MarkOccurrencesHighlighter.level=FINE
private static final Logger LOGGER = Logger.getLogger(MarkOccurrencesHighlighter.class.getName());
public static final Color ERROR_STRIPE_COLOR = new Color(175, 172, 102);
private final DocumentListener documentListener;
private final JTextComponent component;
private final AttributeSet markOccurrencesAttributes;
private final MarkOccurrencesMarkProviderCreator markProviderCreator;
private MarkOccurrencesHighlighter(@NonNull JTextComponent component) {
super((StyledDocument)component.getDocument(), GrammarParserDataDefinitions.CURRENT_RULE_CONTEXT);
Lookup lookup = MimeLookup.getLookup(MimePath.parse(GrammarEditorKit.GRAMMAR_MIME_TYPE));
FontColorSettings settings = lookup.lookup(FontColorSettings.class);
this.documentListener = new ClearHighlightsOnEditListener();
this.component = component;
this.markOccurrencesAttributes = getFontAndColors(settings, "markOccurrences");
Collection<? extends MarkProviderCreator> markProviderCreators = MimeLookup.getLookup(DocumentUtilities.getMimeType(component)).lookupAll(MarkProviderCreator.class);
MarkOccurrencesMarkProviderCreator markOccurrencesMarkProviderCreator = null;
for (MarkProviderCreator creator : markProviderCreators) {
if (creator instanceof MarkOccurrencesMarkProviderCreator) {
markOccurrencesMarkProviderCreator = (MarkOccurrencesMarkProviderCreator)creator;
break;
}
}
this.markProviderCreator = markOccurrencesMarkProviderCreator;
this.getDocument().addDocumentListener(WeakListeners.document(documentListener, this.getDocument()));
}
protected MarkOccurrencesListener createListener(ParserData<? extends CurrentRuleContextData> parserData) {
if (parserData.getContext().getPosition() == null) {
return null;
}
FileModel fileModel = null;
GrammarAnnotatedParseTree annotatedParseTree = null;
try {
Future<ParserData<FileModel>> futureFileModelData = getTaskManager().getData(parserData.getSnapshot(), GrammarParserDataDefinitions.FILE_MODEL, EnumSet.of(ParserDataOptions.NO_UPDATE, ParserDataOptions.SYNCHRONOUS));
ParserData<FileModel> fileModelData = futureFileModelData != null ? futureFileModelData.get() : null;
fileModel = fileModelData != null ? fileModelData.getData() : null;
Future<ParserData<GrammarAnnotatedParseTree>> futureAnnotatedParseTreeData = getTaskManager().getData(parserData.getSnapshot(), GrammarParserDataDefinitions.ANNOTATED_PARSE_TREE, EnumSet.of(ParserDataOptions.NO_UPDATE, ParserDataOptions.SYNCHRONOUS));
ParserData<GrammarAnnotatedParseTree> annotatedParseTreeData = futureAnnotatedParseTreeData != null ? futureAnnotatedParseTreeData.get() : null;
annotatedParseTree = annotatedParseTreeData != null ? annotatedParseTreeData.getData() : null;
} catch (InterruptedException | ExecutionException ex) {
Exceptions.printStackTrace(ex);
}
if (fileModel == null || annotatedParseTree == null) {
return null;
}
return new MarkOccurrencesListener(fileModel, annotatedParseTree, parserData.getContext().getPosition());
}
private final List<SnapshotPosition> markPositions = new ArrayList<>();
@Override
protected void addHighlights(List<Tuple2<OffsetRegion, AttributeSet>> intermediateContainer, DocumentSnapshot sourceSnapshot, DocumentSnapshot currentSnapshot, Collection<Token> tokens, AttributeSet attributes) {
for (Token token : tokens) {
TrackingPositionRegion trackingRegion = sourceSnapshot.createTrackingRegion(OffsetRegion.fromBounds(token.getStartIndex(), token.getStopIndex() + 1), TrackingPositionRegion.Bias.Forward);
SnapshotPositionRegion region = trackingRegion.getRegion(currentSnapshot);
intermediateContainer.add(Tuple.create(region.getRegion(), attributes));
markPositions.add(region.getStart());
}
}
protected void updateHighlights(OffsetsBag container, DocumentSnapshot sourceSnapshot, DocumentSnapshot currentSnapshot, MarkOccurrencesListener listener) {
markPositions.clear();
List<Tuple2<OffsetRegion, AttributeSet>> intermediateContainer = new ArrayList<>(listener.getMarkedOccurrences().size());
addHighlights(intermediateContainer, sourceSnapshot, currentSnapshot, listener.getMarkedOccurrences(), markOccurrencesAttributes);
OffsetsBag updateBag = new OffsetsBag(currentSnapshot.getVersionedDocument().getDocument());
fillHighlights(updateBag, intermediateContainer);
container.setHighlights(updateBag);
Collection<Mark> marks = MarkOccurrencesMarkProvider.createMarks(currentSnapshot.getVersionedDocument(), markPositions, ERROR_STRIPE_COLOR, Bundle.LBL_ERROR_STRIPE_TOOLTIP());
MarkOccurrencesMarkProvider markProvider = markProviderCreator.createMarkProvider(component);
markProvider.setOccurrences(marks);
}
@Override
protected Callable<Void> createAnalyzerTask(@NonNull final ParserData<? extends CurrentRuleContextData> parserData) {
return new Callable<Void>() {
@Override
public Void call() {
final MarkOccurrencesListener listener = createListener(parserData);
if (listener == null) {
return null;
}
try {
ParseTreeWalker.DEFAULT.walk(listener, listener.annotatedParseTree.getParseTree());
} catch (RuntimeException ex) {
Exceptions.printStackTrace(ex);
throw ex;
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
((BaseDocument)getDocument()).render(new Runnable() {
@Override
public void run() {
DocumentSnapshot sourceSnapshot = parserData.getSnapshot();
DocumentSnapshot currentSnapshot = sourceSnapshot.getVersionedDocument().getCurrentSnapshot();
updateHighlights(getContainer(), sourceSnapshot, currentSnapshot, listener);
}
});
}
});
return null;
}
};
}
public static Token getContext(SnapshotPosition position) {
ParserTaskManager taskManager = Lookup.getDefault().lookup(ParserTaskManager.class);
DocumentSnapshot snapshot = position.getSnapshot();
int offset = position.getOffset();
Future<ParserData<Tagger<TokenTag<Token>>>> futureTokensData = taskManager.getData(snapshot, GrammarParserDataDefinitions.LEXER_TOKENS, EnumSet.of(ParserDataOptions.NO_UPDATE, ParserDataOptions.SYNCHRONOUS));
Tagger<TokenTag<Token>> tagger;
try {
ParserData<Tagger<TokenTag<Token>>> tokensData = futureTokensData != null ? futureTokensData.get() : null;
tagger = tokensData != null ? tokensData.getData() : null;
} catch (InterruptedException | ExecutionException ex) {
Exceptions.printStackTrace(ex);
return null;
}
if (tagger == null) {
return null;
}
// get the token(s) at the cursor position, with affinity both directions
OffsetRegion region = OffsetRegion.fromBounds(Math.max(0, offset - 1), Math.min(snapshot.length(), offset + 1));
Iterable<TaggedPositionRegion<TokenTag<Token>>> tags = tagger.getTags(new NormalizedSnapshotPositionRegionCollection(new SnapshotPositionRegion(snapshot, region)));
Token token = null;
for (TaggedPositionRegion<TokenTag<Token>> taggedRegion : tags) {
if (taggedRegion.getTag().getToken().getChannel() != Lexer.DEFAULT_TOKEN_CHANNEL) {
continue;
}
token = taggedRegion.getTag().getToken();
if (token.getStartIndex() <= offset && token.getStopIndex() >= offset) {
break;
}
}
if (token == null) {
// try again without skipping off-channel tokens
for (TaggedPositionRegion<TokenTag<Token>> taggedRegion : tags) {
token = taggedRegion.getTag().getToken();
if (token.getStartIndex() <= offset && token.getStopIndex() >= offset) {
break;
}
}
}
return token;
}
@MimeRegistration(mimeType=GrammarEditorKit.GRAMMAR_MIME_TYPE, service=HighlightsLayerFactory.class)
public static class LayerFactory extends AbstractLayerFactory {
public LayerFactory() {
super(MarkOccurrencesHighlighter.class);
}
@Override
protected AbstractSemanticHighlighter<?> createHighlighter(HighlightsLayerFactory.Context context) {
return new MarkOccurrencesHighlighter(context.getComponent());
}
@Override
protected ZOrder getPosition() {
return ZOrder.CARET_RACK.forPosition(50);
}
}
public static class MarkOccurrencesListener extends GrammarParserBaseListener {
private final FileModel fileModel;
private final GrammarAnnotatedParseTree annotatedParseTree;
private final List<Token> markedOccurrences = new ArrayList<>();
private final Token referencedToken;
public MarkOccurrencesListener(FileModel fileModel, GrammarAnnotatedParseTree annotatedParseTree, SnapshotPosition position) {
this.fileModel = fileModel;
this.annotatedParseTree = annotatedParseTree;
this.referencedToken = findReferencedToken(position);
}
@NonNull
public List<Token> getMarkedOccurrences() {
return markedOccurrences;
}
@Override
public void visitTerminal(TerminalNode node) {
if (referencedToken == null) {
return;
}
if (node.getSymbol().equals(referencedToken)) {
markedOccurrences.add(node.getSymbol());
}
Token target = annotatedParseTree.getTokenDecorator().getProperty(node.getSymbol(), GrammarTreeProperties.PROP_TARGET);
if (target != null && target.equals(referencedToken)) {
markedOccurrences.add(node.getSymbol());
}
}
private Token findReferencedToken(SnapshotPosition position) {
Token currentToken = getContext(position);
if (currentToken == null) {
return null;
}
if (annotatedParseTree.isDefinition(currentToken)) {
return currentToken;
}
return annotatedParseTree.getTokenDecorator().getProperty(currentToken, GrammarTreeProperties.PROP_TARGET);
}
}
private class ClearHighlightsOnEditListener implements DocumentListener {
@Override
public void insertUpdate(DocumentEvent e) {
clearHighlights(e.getOffset());
}
@Override
public void removeUpdate(DocumentEvent e) {
clearHighlights(e.getOffset());
}
@Override
public void changedUpdate(DocumentEvent e) {
clearHighlights(e.getOffset());
}
private void clearHighlights(int offset) {
OffsetsBag container = getContainer();
HighlightsSequence sequence = container.getHighlights(0, Integer.MAX_VALUE);
if (!sequence.moveNext()) {
// no highlights
return;
}
if (sequence.getEndOffset() >= offset) {
container.clear();
return;
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
getContainer().clear();
}
});
}
}
}