/*
* 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.antlr4.formatting;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.StyledDocument;
import org.antlr.netbeans.editor.classification.TokenTag;
import org.antlr.netbeans.editor.completion.Anchor;
import org.antlr.netbeans.editor.completion.ReferenceAnchors;
import org.antlr.netbeans.editor.formatting.CodeStyle;
import org.antlr.netbeans.editor.tagging.Tagger;
import org.antlr.netbeans.editor.text.DocumentSnapshot;
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.VersionedDocument;
import org.antlr.netbeans.editor.text.VersionedDocumentUtilities;
import org.antlr.netbeans.parsing.spi.ParserTaskManager;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.RuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenSource;
import org.antlr.v4.runtime.misc.Interval;
import org.antlr.v4.runtime.misc.Tuple2;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.TerminalNode;
import org.antlr.works.editor.antlr4.classification.TaggerTokenSource;
import org.antlr.works.editor.antlr4.completion.CaretReachedException;
import org.antlr.works.editor.antlr4.completion.CaretToken;
import org.antlr.works.editor.antlr4.completion.CodeCompletionTokenSource;
import org.antlr.works.editor.antlr4.parsing.ParseTrees;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.modules.editor.indent.spi.Context;
import org.netbeans.modules.editor.indent.spi.ExtraLock;
import org.netbeans.modules.editor.indent.spi.IndentTask;
import org.openide.text.NbDocument;
import org.openide.util.Lookup;
import org.openide.util.NotImplementedException;
import org.openide.util.Parameters;
/**
*
* @author Sam Harwell
*/
public abstract class AbstractIndentTask implements IndentTask {
// -J-Dorg.antlr.works.editor.antlr4.formatting.AbstractIndentTask.level=FINE
protected static final Logger LOGGER = Logger.getLogger(AbstractIndentTask.class.getName());
private final Context _context;
private final ParserTaskManager _taskManager;
private DocumentSnapshot _snapshot;
protected AbstractIndentTask(@NonNull Context context) {
this(context, Lookup.getDefault().lookup(ParserTaskManager.class));
}
protected AbstractIndentTask(@NonNull Context context, @NonNull ParserTaskManager taskManager) {
Parameters.notNull("context", context);
Parameters.notNull("taskManager", taskManager);
_context = context;
_taskManager = taskManager;
}
public final Context getContext() {
return _context;
}
public final ParserTaskManager getTaskManager() {
return _taskManager;
}
public final DocumentSnapshot getSnapshot() {
if (_snapshot == null) {
VersionedDocument versionedDocument = VersionedDocumentUtilities.getVersionedDocument(getContext().document());
_snapshot = versionedDocument.getCurrentSnapshot();
}
return _snapshot;
}
@Override
public final void reindent() throws BadLocationException {
if (!smartReindent()) {
fallbackReindent();
}
}
@Override
public ExtraLock indentLock() {
return null;
}
public boolean smartReindent() throws BadLocationException {
if (!(getContext().document() instanceof StyledDocument)) {
return false;
}
StyledDocument document = (StyledDocument)getContext().document();
SnapshotPosition contextEndPosition = new SnapshotPosition(getSnapshot(), getContext().endOffset());
SnapshotPosition endPosition = contextEndPosition.getContainingLine().getEndIncludingLineBreak();
SnapshotPosition endPositionOnLine = contextEndPosition.getContainingLine().getEnd();
ReferenceAnchors anchors = findNearestAnchors(getTaskManager(), getSnapshot(), endPosition.getOffset());
final Anchor previous = anchors.getPrevious();
Tagger<TokenTag<Token>> tagger = getTagger();
if (tagger == null) {
return false;
}
int regionEnd = Math.min(getSnapshot().length(), endPosition.getOffset() + 1);
OffsetRegion region;
Anchor enclosing = anchors.getEnclosing();
if (enclosing != null) {
region = OffsetRegion.fromBounds(enclosing.getSpan().getStartPosition(getSnapshot()).getOffset(), regionEnd);
} else if (previous != null) {
// at least for now, include the previous span due to the way error handling places bounds on an anchor
region = OffsetRegion.fromBounds(previous.getSpan().getStartPosition(getSnapshot()).getOffset(), regionEnd);
} else {
region = OffsetRegion.fromBounds(0, regionEnd);
}
LOGGER.log(Level.FINE, "Reindent from anchor region: {0}.", region);
TaggerTokenSource taggerTokenSource = new TaggerTokenSource(tagger, new SnapshotPositionRegion(getSnapshot(), region));
TokenSource tokenSource = new CodeCompletionTokenSource(endPosition.getOffset(), taggerTokenSource);
CommonTokenStream tokens = new CommonTokenStream(tokenSource);
Map<RuleContext, CaretReachedException> parseTrees = getParseTrees(tokens, anchors);
if (parseTrees == null) {
return false;
}
NavigableMap<Integer, List<Map.Entry<RuleContext, CaretReachedException>>> indentLevels = new TreeMap<>();
for (Map.Entry<RuleContext, CaretReachedException> parseTree : parseTrees.entrySet()) {
if (parseTree.getValue() == null) {
continue;
}
ParseTree firstNodeOnLine = findFirstNodeAfterOffset(parseTree.getKey(), endPositionOnLine.getContainingLine().getStart().getOffset());
if (firstNodeOnLine == null) {
firstNodeOnLine = parseTree.getValue().getFinalContext();
}
if (firstNodeOnLine == null) {
continue;
}
int indentationLevel = getIndent(parseTree, firstNodeOnLine, getContext().lineStartOffset(getContext().endOffset()));
if (indentationLevel < 0) {
continue;
}
List<Map.Entry<RuleContext, CaretReachedException>> indentList =
indentLevels.get(indentationLevel);
if (indentList == null) {
indentList = new ArrayList<>();
indentLevels.put(indentationLevel, indentList);
}
indentList.add(parseTree);
}
if (indentLevels.isEmpty()) {
return false;
}
int indentLevel = indentLevels.firstKey();
if (indentLevels.size() > 1) {
// TODO: resolve multiple possibilities
}
int startLine = NbDocument.findLineNumber(document, getContext().startOffset());
int endLine;
if (getContext().endOffset() <= getContext().startOffset()) {
endLine = startLine;
} else {
endLine = NbDocument.findLineNumber(document, getContext().endOffset() - 1);
}
for (int line = startLine; line <= endLine; line++) {
int currentOffset = NbDocument.findLineOffset(document, startLine);
getContext().modifyIndent(currentOffset, indentLevel);
}
return true;
}
protected ReferenceAnchors findNearestAnchors(ParserTaskManager taskManager, DocumentSnapshot snapshot, int endOffset) {
List<? extends Anchor> anchors = getDynamicAnchorPoints();
// the innermost anchor enclosing the caret
Anchor enclosing = null;
// the last anchor starting before the caret
Anchor previous = null;
if (anchors != null) {
Anchor next = null;
/*
* parse the current rule
*/
for (Anchor anchor : anchors) {
if (anchor.getSpan().getStartPosition(snapshot).getOffset() <= endOffset) {
previous = anchor;
if (anchor.getSpan().getEndPosition(snapshot).getOffset() > endOffset) {
enclosing = anchor;
}
} else {
next = anchor;
break;
}
}
}
return new ReferenceAnchors(previous, enclosing);
}
protected TerminalNode findFirstNodeAfterOffset(ParseTree tree, int offset) {
TerminalNode lastNode = ParseTrees.getStopNode(tree);
if (lastNode == null) {
return null;
}
if (lastNode.getSymbol() instanceof CaretToken) {
throw new NotImplementedException();
} else if (lastNode.getSymbol().getStartIndex() < offset) {
return null;
}
if (tree instanceof TerminalNode) {
return (TerminalNode)tree;
}
for (int i = 0; i < tree.getChildCount(); i++) {
TerminalNode node = findFirstNodeAfterOffset(tree.getChild(i), offset);
if (node != null) {
return node;
}
}
return null;
}
protected boolean isMultilineElement(ParseTree tree) throws BadLocationException {
TerminalNode startNode = ParseTrees.getStartNode(tree);
TerminalNode stopNode = ParseTrees.getStopNode(tree);
if (startNode == null || stopNode == null) {
// TODO: this can't handle epsilon trees (no TerminalNode descendants)
return false;
}
int startLine = startNode.getSymbol().getLine();
int stopLine = stopNode.getSymbol().getLine();
if (stopLine != startLine) {
return true;
}
return isMultilineElement(stopNode.getSymbol());
}
protected boolean isMultilineElement(Token token) throws BadLocationException {
return getContext().lineStartOffset(token.getStartIndex()) != getContext().lineStartOffset(token.getStopIndex());
}
protected abstract Tagger<TokenTag<Token>> getTagger();
protected abstract CodeStyle getCodeStyle();
protected abstract Map<RuleContext, CaretReachedException> getParseTrees(CommonTokenStream tokens, ReferenceAnchors anchors);
protected abstract List<? extends Anchor> getDynamicAnchorPoints();
protected abstract Set<AlignmentRequirement> getAlignmentRequirement(Map.Entry<RuleContext, CaretReachedException> parseTree, @NonNull ParseTree targetElement, ParseTree ancestor);
/**
* Gets the target element and offset which controls the indentation of
* {@code targetElement}.
* <p/>
* Note: The search for ancestor elements continues when the set returned by
* {@link #getAlignmentRequirement} contains
* {@link AlignmentRequirement#USE_ANCESTOR}.
* <p/>
* Note: When {@code priorSiblings} is not {@code null}, the last element of
* the list is an ancestor of {@code targetElement}.
* <p/>
* Note: It is an error if the returned element starts after
* {@code targetElement} in the document. If the returned element starts on
* the same line as {@code targetElement}, the search continues with the
* parent of {@code container}.
*
* @param parseTree
* @param targetElement The element requiring alignment.
* @param container An ancestor element containing the target element.
* @param priorSiblings The children of {@code container} from the first
* child through the the child containing {@code targetElement}. The last
* element of {@code priorSiblings} is an ancestor of {@code targetElement}.
* This argument may be {@code null} when the set returned by
* {@link #getAlignmentRequirement} does not contain
* {@link AlignmentRequirement#PRIOR_SIBLING}.
*
* @return Returns a tuple; the first element is the target element
* controlling vertical alignment of the current line, and the second
* element specifies an offset from the horizontal position of this element
* in virtual spaces. Return {@code null} to continue searching ancestors.
*/
@CheckForNull
protected abstract Tuple2<? extends ParseTree, Integer> getAlignmentElement(Map.Entry<RuleContext, CaretReachedException> parseTree, @NonNull ParseTree targetElement, @NonNull ParseTree container, @NullAllowed List<? extends ParseTree> priorSiblings);
protected int getIndent(final Map.Entry<RuleContext, CaretReachedException> parseTree, final ParseTree firstNodeOnLine, int lineStartOffset) throws BadLocationException {
for (ParseTree ancestor = firstNodeOnLine; ancestor != null; ancestor = ancestor.getParent()) {
Set<AlignmentRequirement> requirements = getAlignmentRequirement(parseTree, firstNodeOnLine, ancestor);
if (requirements.contains(AlignmentRequirement.USE_ANCESTOR)) {
continue;
} else if (requirements.contains(AlignmentRequirement.IGNORE_TREE)) {
return -1;
}
List<ParseTree> siblings = null;
if (requirements.contains(AlignmentRequirement.PRIOR_SIBLING)) {
int childCount = ancestor.getChildCount();
siblings = new ArrayList<>(childCount);
for (int i = 0; i < childCount; i++) {
ParseTree child = ancestor.getChild(i);
siblings.add(child);
if (ParseTrees.isAncestorOf(child, firstNodeOnLine)) {
break;
}
}
}
Tuple2<? extends ParseTree, Integer> alignmentElement = getAlignmentElement(parseTree, firstNodeOnLine, ancestor, siblings);
if (alignmentElement == null) {
continue;
}
Token startToken = ParseTrees.getStartSymbol(alignmentElement.getItem1());
String beginningOfLineText = startToken.getTokenSource().getInputStream().getText(new Interval(startToken.getStartIndex() - startToken.getCharPositionInLine(), startToken.getStartIndex() - 1));
int elementIndent = 0;
for (int i = 0; i < beginningOfLineText.length(); i++) {
if (beginningOfLineText.charAt(i) == '\t') {
elementIndent = getCodeStyle().getIndentSize() * (elementIndent / getCodeStyle().getIndentSize() + 1);
} else {
elementIndent++;
}
}
if (ParseTrees.getStartSymbol(firstNodeOnLine) == startToken) {
LOGGER.log(Level.WARNING, "Attempting to indent a line relative to an element on that line.");
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.log(Level.FINE, "Indent {0} relative to {1} (offset {2}) => {3}", new Object[] { firstNodeOnLine, alignmentElement.getItem1(), alignmentElement.getItem2(), elementIndent + alignmentElement.getItem2() });
}
return elementIndent + alignmentElement.getItem2();
}
return -1;
}
protected void fallbackReindent() throws BadLocationException {
if (!(getContext().document() instanceof StyledDocument)) {
return;
}
StyledDocument document = (StyledDocument)getContext().document();
int startLine = NbDocument.findLineNumber(document, getContext().startOffset());
int endLine;
if (getContext().endOffset() <= getContext().startOffset()) {
endLine = startLine;
} else {
endLine = NbDocument.findLineNumber(document, getContext().endOffset() - 1);
}
int previousIndent;
if (startLine == 0) {
previousIndent = 0;
} else {
int previousLineOffset = NbDocument.findLineOffset(document, startLine - 1);
previousIndent = getContext().lineIndent(previousLineOffset);
}
for (int line = startLine; line <= endLine; line++) {
int currentOffset = NbDocument.findLineOffset(document, startLine);
int currentIndent = getContext().lineIndent(currentOffset);
if (currentIndent == 0 && previousIndent > 0) {
getContext().modifyIndent(currentOffset, previousIndent);
}
previousIndent = currentIndent;
}
}
}