/*
* 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.antlr3.highlighting;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.StyledDocument;
import org.antlr.netbeans.editor.highlighting.Highlight;
import org.antlr.netbeans.editor.highlighting.HighlightsList;
import org.antlr.netbeans.editor.highlighting.LineStateInfo;
import org.antlr.netbeans.editor.highlighting.ParseRequest;
import org.antlr.netbeans.editor.highlighting.SingleHighlightSequence;
import org.antlr.netbeans.editor.text.OffsetRegion;
import org.antlr.runtime.CharStream;
import org.antlr.runtime.CommonToken;
import org.antlr.runtime.Token;
import org.antlr.v4.runtime.misc.Interval;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.spi.editor.highlighting.HighlightsSequence;
import org.netbeans.spi.editor.highlighting.support.AbstractHighlightsContainer;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.Parameters;
/**
*
* @author Sam Harwell
* @param <TState>
*/
public abstract class ANTLRHighlighterBase<TState extends LineStateInfo<TState>> extends AbstractHighlightsContainer {
private static final boolean FULL_CHECKS = false;
private static final boolean FIX_HIGHLIGHTER_UPDATE_BUG = false;
private static boolean timeoutReported;
private final Object lock = new Object();
private final StyledDocument document;
private final DocumentListenerImpl documentListener;
private final ArrayList<TState> lineStates = new ArrayList<>();
private final boolean propagateChangedImmediately;
private Integer firstDirtyLine;
private Integer lastDirtyLine;
private Integer firstChangedLine;
private Integer lastChangedLine;
private boolean failedTimeout;
public ANTLRHighlighterBase(@NonNull StyledDocument document) {
this(document, true);
}
public ANTLRHighlighterBase(@NonNull StyledDocument document, boolean propagateChanges) {
Parameters.notNull("document", document);
this.document = document;
this.propagateChangedImmediately = propagateChanges;
this.documentListener = new DocumentListenerImpl();
}
@NonNull
protected final StyledDocument getDocument() {
return document;
}
public void initialize() {
TState dirtyState = getStartState().createDirtyState();
int lineCount = NbDocument.findLineRootElement(this.document).getElementCount();
this.lineStates.ensureCapacity(lineCount);
for (int i = 0; i < lineCount; i++) {
this.lineStates.add(dirtyState);
}
subscribeEvents();
firstDirtyLine = 0;
lastDirtyLine = lineStates.size() - 1;
forceRehighlightLines(0, lineStates.size() - 1);
}
@Override
public HighlightsSequence getHighlights(int startOffset, int endOffset) {
List<Highlight> highlights = new ArrayList<>();
getHighlights(startOffset, endOffset, highlights, null, true, false);
return new HighlightsList(highlights);
}
@CheckForNull
public Interval getHighlights(int startOffset, int endOffset, List<Highlight> highlights, List<Token> tokens, boolean updateOffsets, boolean propagate) {
if (highlights == null && tokens == null && !propagate) {
return null;
}
if (endOffset == Integer.MAX_VALUE) {
endOffset = document.getLength();
}
if (highlights != null) {
highlights.clear();
}
if (tokens != null) {
tokens.clear();
}
OffsetRegion span = OffsetRegion.fromBounds(startOffset, endOffset);
if (failedTimeout) {
return null;
}
int firstUpdatedLine;
int lastUpdatedLine;
boolean spanExtended = false;
int extendMultiLineSpanToLine = 0;
OffsetRegion extendedSpan = span;
synchronized (lock) {
OffsetRegion requestedSpan = span;
ParseRequest<TState> request = adjustParseSpan(span);
TState startState = request.getState();
span = request.getRegion();
firstUpdatedLine = NbDocument.findLineNumber(document, span.getStart());
lastUpdatedLine = NbDocument.findLineNumber(document, span.getEnd());
CharStream input;
try {
input = createInputStream(span);
} catch (BadLocationException ex) {
return null;
}
TokenSourceWithState<TState> lexer = createLexer(input, startState);
CommonToken previousToken = null;
// int previousTokenLine = 0;
boolean previousTokenEndsLine = false;
/* this is held outside the loop because only tokens which end at the end of a line
* impact its value.
*/
boolean lineStateChanged = false;
while (true)
{
// TODO: perform this under a read lock
CommonToken token = (CommonToken)lexer.nextToken();
// The latter is true for EOF token with span.getEnd() at the end of the document
boolean inBounds = token.getStartIndex() < span.getEnd()
|| token.getStopIndex() < span.getEnd();
if (updateOffsets) {
int startLineCurrent;
if (token.getType() == Token.EOF)
startLineCurrent = NbDocument.findLineRootElement(document).getElementCount();
else
startLineCurrent = NbDocument.findLineNumber(document, token.getStartIndex());
// endLinePrevious is the line number the previous token ended on
int endLinePrevious;
if (previousToken != null)
endLinePrevious = NbDocument.findLineNumber(document, previousToken.getStopIndex());
else
endLinePrevious = NbDocument.findLineNumber(document, span.getStart()) - 1;
if (startLineCurrent > endLinePrevious + 1 || (startLineCurrent == endLinePrevious + 1 && !previousTokenEndsLine))
{
int firstMultilineLine = endLinePrevious;
if (previousToken == null || previousTokenEndsLine)
firstMultilineLine++;
for (int i = firstMultilineLine; i < startLineCurrent; i++)
{
if (!lineStates.get(i).getIsMultiLineToken() || lineStateChanged)
extendMultiLineSpanToLine = i + 1;
if (inBounds || propagate)
setLineState(i, lineStates.get(i).createMultiLineState());
}
}
}
if (token.getType() == Token.EOF)
break;
if (updateOffsets && isMultiLineToken(lexer, token))
{
int startLine = NbDocument.findLineNumber(document, token.getStartIndex());
int stopLine = NbDocument.findLineNumber(document, token.getStopIndex());
for (int i = startLine; i < stopLine; i++)
{
if (!lineStates.get(i).getIsMultiLineToken())
extendMultiLineSpanToLine = i + 1;
if (inBounds || propagate)
setLineState(i, lineStates.get(i).createMultiLineState());
}
}
boolean tokenEndsLine = tokenEndsAtEndOfLine(lexer, token);
if (updateOffsets && tokenEndsLine)
{
TState stateAtEndOfLine = lexer.getState();
int line = NbDocument.findLineNumber(document, token.getStopIndex());
lineStateChanged =
lineStates.get(line).getIsMultiLineToken()
|| !lineStates.get(line).equals(stateAtEndOfLine);
// even if the state didn't change, we call SetLineState to make sure the _first/_lastChangedLine values get updated.
// have to check bounds for this one or the editor might not get an update (if the token ends a line)
if (updateOffsets && (inBounds || propagate))
setLineState(line, stateAtEndOfLine);
if (lineStateChanged)
{
if (line < NbDocument.findLineRootElement(document).getElementCount() - 1)
{
/* update the span's end position or the line state change won't be reflected
* in the editor
*/
int endPosition = line < NbDocument.findLineRootElement(document).getElementCount() - 2 ? NbDocument.findLineOffset(document, line + 2) : document.getLength();
if (endPosition > extendedSpan.getEnd())
{
spanExtended = true;
extendedSpan = OffsetRegion.fromBounds(extendedSpan.getStart(), endPosition);
}
}
}
}
previousToken = token;
previousTokenEndsLine = tokenEndsLine;
boolean canBreak = !propagate || !spanExtended;
if (propagate && spanExtended) {
span = OffsetRegion.fromBounds(span.getStart(), extendedSpan.getEnd());
lastUpdatedLine = NbDocument.findLineNumber(document, span.getEnd());
spanExtended = false;
}
if (canBreak && (token.getStartIndex() >= span.getEnd())) {
break;
}
if (token.getStopIndex() < requestedSpan.getStart()) {
continue;
}
if (tokens != null) {
tokens.add(token);
}
if (highlights != null) {
Collection<Highlight> tokenClassificationSpans = getHighlightsForToken(token);
if (tokenClassificationSpans != null) {
highlights.addAll(tokenClassificationSpans);
}
}
if (canBreak && !inBounds) {
break;
}
}
}
if (updateOffsets && extendMultiLineSpanToLine > 0 && !propagate) {
int endPosition = extendMultiLineSpanToLine < NbDocument.findLineRootElement(document).getElementCount() - 1 ? NbDocument.findLineOffset(document, extendMultiLineSpanToLine + 1) : document.getLength();
if (endPosition > extendedSpan.getEnd()) {
spanExtended = true;
extendedSpan = OffsetRegion.fromBounds(extendedSpan.getStart(), endPosition);
}
}
if (updateOffsets && spanExtended) {
/* Subtract 1 from each of these because the spans include the line break on their last
* line, forcing it to appear as the first position on the following line.
*/
int firstLine = NbDocument.findLineNumber(document, span.getEnd());
int lastLine = NbDocument.findLineNumber(document, extendedSpan.getEnd()) - 1;
forceRehighlightLines(firstLine, lastLine);
}
return new Interval(firstUpdatedLine, lastUpdatedLine);
}
protected void setLineState(int line, TState state) {
synchronized (lock) {
checkDirtyLineBounds();
assert firstDirtyLine == null || line <= firstDirtyLine || state.getIsDirty();
lineStates.set(line, state);
if (!state.getIsDirty() && firstDirtyLine != null && firstDirtyLine.equals(line)) {
firstDirtyLine++;
}
if (!state.getIsDirty() && lastDirtyLine != null && lastDirtyLine.equals(line)) {
assert firstDirtyLine != null && firstDirtyLine == lastDirtyLine + 1;
firstDirtyLine = null;
lastDirtyLine = null;
}
checkDirtyLineBounds();
}
}
private void checkDirtyLineBounds() {
if (!FULL_CHECKS) {
return;
}
if (firstDirtyLine == null) {
if (lastDirtyLine != null) {
throw new IllegalStateException();
}
for (int i = 0; i < lineStates.size(); i++) {
if (lineStates.get(i).getIsDirty()) {
throw new IllegalStateException();
}
}
return;
}
if (lastDirtyLine == null) {
throw new IllegalStateException();
}
for (int i = 0; i < firstDirtyLine; i++) {
if (lineStates.get(i).getIsDirty()) {
throw new IllegalStateException();
}
}
for (int i = lastDirtyLine + 1; i < lineStates.size(); i++) {
if (lineStates.get(i).getIsDirty()) {
throw new IllegalStateException();
}
}
}
protected abstract TState getStartState();
protected ParseRequest<TState> adjustParseSpan(OffsetRegion span) {
int start = span.getStart();
int end = span.getEnd();
if (firstDirtyLine != null) {
int firstDirtyLineOffset = NbDocument.findLineOffset(document, firstDirtyLine);
start = Math.min(start, firstDirtyLineOffset);
}
TState state = null;
int startLine = NbDocument.findLineNumber(document, start);
while (startLine > 0) {
TState lineState = lineStates.get(startLine - 1);
if (!lineState.getIsMultiLineToken()) {
state = lineState;
break;
}
startLine--;
}
if (startLine == 0) {
state = getStartState();
}
start = NbDocument.findLineOffset(document, startLine);
int length = end - start;
ParseRequest<TState> request = new ParseRequest<>(new OffsetRegion(start, length), state);
return request;
}
protected boolean tokenSkippedLines(int endLinePrevious, CommonToken token) {
int startLineCurrent = NbDocument.findLineNumber(this.document, token.getStartIndex());
return startLineCurrent > endLinePrevious + 1;
}
protected boolean isMultiLineToken(TokenSourceWithState<TState> lexer, CommonToken token) {
/*if (lexer != null && lexer.getCharStream().getLine() > token.getLine()) {
return true;
}*/
int startLine = NbDocument.findLineNumber(this.document, token.getStartIndex());
int stopLine = NbDocument.findLineNumber(this.document, token.getStopIndex() + 1);
return startLine != stopLine;
}
protected boolean tokenEndsAtEndOfLine(TokenSourceWithState<TState> lexer, CommonToken token) {
CharStream charStream = lexer.getCharStream();
if (charStream != null) {
int nextCharIndex = token.getStopIndex() + 1;
if (nextCharIndex >= charStream.size()) {
return true;
}
int c = charStream.substring(token.getStopIndex() + 1, token.getStopIndex() + 1).charAt(0);
return c == '\r' || c == '\n';
}
if (token.getStopIndex() + 1 >= document.getLength()) {
return true;
}
try {
char c = document.getText(token.getStopIndex() + 1, 1).charAt(0);
return c == '\r' || c == '\n';
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
return false;
}
/*int line = NbDocument.findLineNumber(document, token.getStopIndex());
int lineStart = NbDocument.findLineOffset(document, line);
int nextLineStart = NbDocument.findLineOffset(document, line + 1);
int lineEnd = nextLineStart - 1;
if (lineEnd > 0 && lineEnd > lineStart) {
try {
char c = document.getText(lineEnd - 1, 1).charAt(0);
if (c == '\r' || c == '\n') {
lineEnd--;
}
} catch (BadLocationException ex) {
Exceptions.printStackTrace(ex);
}
}
return lineEnd <= token.getStopIndex() + 1 && nextLineStart >= token.getStopIndex() + 1;*/
}
protected CharStream createInputStream(OffsetRegion span) throws BadLocationException {
CharStream input;
if (span.getLength() > 1000) {
input = new DocumentCharStream(this.document, span);
} else {
input = new DocumentCharStream(this.document);
}
input.seek(span.getStart());
return input;
}
protected abstract TokenSourceWithState<TState> createLexer(CharStream input, TState startState);
protected Collection<Highlight> getHighlightsForToken(CommonToken token) {
AttributeSet attributes = highlightToken(token);
if (attributes != null && attributes.getAttributeCount() > 0) {
return new SingleHighlightSequence(new Highlight(token.getStartIndex(), token.getStopIndex() + 1, attributes));
}
return Collections.emptyList();
}
protected AttributeSet highlightToken(CommonToken token) {
return null;
}
public void forceRehighlightLines(int startLine, int endLineInclusive) {
checkDirtyLineBounds();
firstDirtyLine = firstDirtyLine != null ? Math.min(firstDirtyLine, startLine) : startLine;
lastDirtyLine = lastDirtyLine != null ? Math.max(lastDirtyLine, endLineInclusive) : endLineInclusive;
int start = NbDocument.findLineOffset(document, startLine);
int end = (endLineInclusive == lineStates.size() - 1) ? document.getLength() : NbDocument.findLineOffset(document, endLineInclusive + 1);
if (FIX_HIGHLIGHTER_UPDATE_BUG) {
fireHighlightsChange(start, document.getLength());
} else {
fireHighlightsChange(start, end);
}
checkDirtyLineBounds();
}
protected void subscribeEvents() {
this.document.addDocumentListener(this.documentListener);
}
protected void unsubscribeEvents() {
this.document.removeDocumentListener(this.documentListener);
}
private final class DocumentListenerImpl implements DocumentListener {
@Override
public void changedUpdate(DocumentEvent e) {
int lineCountDelta = NbDocument.findLineRootElement(document).getElementCount() - lineStates.size();
int oldOffset = e.getOffset();
int oldLength = e.getLength();
int newOffset = e.getOffset();
int newLength = e.getLength();
processChange(lineCountDelta, oldOffset, oldLength, newOffset, newLength);
processAfterChange();
}
@Override
public void insertUpdate(DocumentEvent e) {
int lineCountDelta = NbDocument.findLineRootElement(document).getElementCount() - lineStates.size();
int oldOffset = e.getOffset();
int oldLength = 0;
int newOffset = e.getOffset();
int newLength = e.getLength();
processChange(lineCountDelta, oldOffset, oldLength, newOffset, newLength);
processAfterChange();
}
@Override
public void removeUpdate(DocumentEvent e) {
int lineCountDelta = NbDocument.findLineRootElement(document).getElementCount() - lineStates.size();
int oldOffset = e.getOffset();
int oldLength = e.getLength();
int newOffset = e.getOffset();
int newLength = 0;
processChange(lineCountDelta, oldOffset, oldLength, newOffset, newLength);
processAfterChange();
}
private void processChange(int lineCountDelta, int oldOffset, int oldLength, int newOffset, int newLength) {
synchronized (lock) {
int lineNumberFromPosition = NbDocument.findLineNumber(document, newOffset);
int num2 = NbDocument.findLineNumber(document, newOffset + newLength);
if (lineCountDelta < 0) {
lineStates.subList(lineNumberFromPosition, lineNumberFromPosition + Math.abs(lineCountDelta)).clear();
} else if (lineCountDelta > 0) {
TState endLineState = lineStates.get(lineNumberFromPosition);
List<TState> insertedElements = new ArrayList<>();
for (int i = 0; i < lineCountDelta; i++) {
insertedElements.add(endLineState);
}
lineStates.addAll(lineNumberFromPosition, insertedElements);
}
if (lastDirtyLine != null && lastDirtyLine > lineNumberFromPosition) {
lastDirtyLine += lineCountDelta;
}
if (lastChangedLine != null && lastChangedLine > lineNumberFromPosition) {
lastChangedLine += lineCountDelta;
}
for (int i = lineNumberFromPosition; i <= num2; i++) {
TState state = lineStates.get(i);
lineStates.set(i, state.createDirtyState());
}
firstDirtyLine = firstDirtyLine != null ? Math.min(firstDirtyLine, lineNumberFromPosition) : lineNumberFromPosition;
lastDirtyLine = lastDirtyLine != null ? Math.max(lastDirtyLine, num2) : num2;
firstChangedLine = firstChangedLine != null ? Math.min(firstChangedLine, lineNumberFromPosition) : lineNumberFromPosition;
lastChangedLine = lastChangedLine != null ? Math.max(lastChangedLine, num2) : num2;
}
}
private void processAfterChange() {
if (firstChangedLine != null && lastChangedLine != null) {
int startRehighlightLine = firstChangedLine;
int endRehighlightLine = Math.min(lastChangedLine, NbDocument.findLineRootElement(document).getElementCount() - 1);
firstChangedLine = null;
lastChangedLine = null;
if (propagateChangedImmediately) {
if (firstDirtyLine != null) {
startRehighlightLine = Math.min(startRehighlightLine, firstDirtyLine);
}
if (lastDirtyLine != null) {
endRehighlightLine = Math.max(endRehighlightLine, lastDirtyLine);
}
int startOffset = NbDocument.findLineOffset(document, startRehighlightLine);
int endOffset;
if (endRehighlightLine == NbDocument.findLineRootElement(document).getElementCount() - 1) {
endOffset = document.getLength();
} else {
endOffset = NbDocument.findLineOffset(document, endRehighlightLine + 1) - 1;
}
Interval propagatedChangedLines = getHighlights(startOffset, endOffset, null, null, true, true);
if (propagatedChangedLines != null) {
forceRehighlightLines(propagatedChangedLines.a, propagatedChangedLines.b);
return;
}
}
forceRehighlightLines(startRehighlightLine, endRehighlightLine);
}
}
}
}