package org.eclipse.dltk.ui.text.heredoc;
import java.util.Arrays;
import java.util.ListIterator;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TypedPosition;
import org.eclipse.jface.text.rules.FastPartitioner;
import org.eclipse.jface.text.rules.IPartitionTokenScanner;
import org.eclipse.jface.text.rules.IToken;
/**
* A slightly modified implemenation of the <code>FastPartitioner</code> that
* can properly partition heredoc.
*
* <p>
* There is no need to use this partitioner if heredoc is not supported by the
* underlying dynamic language.
* </p>
*
* <p>
* If you do use this partitioner, you <b>must</b> also use the
* <code>HereDocEnabledPartitionScanner</code> partition scanner implemenation
* it requires a heredoc specific partitioning rule and knows how to properly
* buffer tokens in the event other partitions are found on the heredoc
* identifier line, ie:
* </p>
*
* <pre>
* <<EOF . "hello world\n";
* heredoc body
* EOF
*
* <<ABC . "hello world\n" . <<XYZ;
* abc body
* ABC
* xyz body
* XYZ
* </pre>
*
* @see HereDocPartitionRule
* @see HereDocEnabledPartitionScanner
* @see HereDocEnabledPresentationReconciler
*/
public class HereDocEnabledPartitioner extends FastPartitioner {
private boolean fIsInitialized;
private String fPositionCategory;
private HereDocEnabledPartitionScanner fScanner;
public HereDocEnabledPartitioner(IPartitionTokenScanner scanner,
String[] legalContentTypes) {
super(scanner, legalContentTypes);
// you can't have one w/o the other...
Assert.isTrue(scanner instanceof HereDocEnabledPartitionScanner);
fScanner = (HereDocEnabledPartitionScanner) scanner;
// meh, why isn't this a protected field in parent?
fPositionCategory = getManagingPositionCategories()[0];
}
@Override public IRegion documentChanged2(DocumentEvent e) {
if (!fIsInitialized)
return null;
try {
Assert.isTrue(e.getDocument() == fDocument);
Position[] category = getPositions();
IRegion line = fDocument.getLineInformationOfOffset(e.getOffset());
int reparseStart = line.getOffset();
int partitionStart = -1;
String contentType = null;
int newLength = (e.getText() == null) ? 0 : e.getText().length();
int first = fDocument.computeIndexInCategory(fPositionCategory,
reparseStart);
if (first > 0) {
TypedPosition partition = (TypedPosition) category[first - 1];
// heredoc partitions trump all others...
if (HereDocUtils.isHereDocContent(partition.getType())) {
reparseStart = findReparseStartForHereDoc(category, first,
partition);
partitionStart = reparseStart;
--first;
} else if (partition.includes(reparseStart)) {
partitionStart = partition.getOffset();
contentType = partition.getType();
if (e.getOffset() == (partition.getOffset() + partition
.getLength()))
reparseStart = partitionStart;
--first;
} else if ((reparseStart == e.getOffset())
&& (reparseStart == (partition.getOffset() + partition
.getLength()))) {
partitionStart = partition.getOffset();
contentType = partition.getType();
reparseStart = partitionStart;
--first;
} else {
/*
* the partition will start wherever we begin reparsing
* otherwise interrurpting the heredoc identifier won't be
* reparsed correctly, ie: '<a<ABC'
*/
partitionStart = reparseStart;
contentType = IDocument.DEFAULT_CONTENT_TYPE;
}
}
fPositionUpdater.update(e);
for (int i = first; i < category.length; i++) {
Position p = category[i];
if (p.isDeleted) {
rememberDeletedOffset(e.getOffset());
break;
}
}
clearPositionCache();
category = getPositions();
fScanner.setPartialRange(fDocument, reparseStart,
fDocument.getLength() - reparseStart, contentType,
partitionStart);
int behindLastScannedPosition = reparseStart;
IToken token = fScanner.nextToken();
while (!token.isEOF()) {
contentType = getTokenContentType(token);
if (!isSupportedContentType(contentType)) {
token = fScanner.nextToken();
continue;
}
int start = fScanner.getTokenOffset();
int length = fScanner.getTokenLength();
behindLastScannedPosition = start + length;
int lastScannedPosition = behindLastScannedPosition - 1;
// remove all affected positions
while (first < category.length) {
TypedPosition p = (TypedPosition) category[first];
if ((lastScannedPosition >= (p.offset + p.length))
|| (p.overlapsWith(start, length) && (!fDocument
.containsPosition(fPositionCategory, start,
length) || !contentType.equals(p
.getType())))) {
rememberRegion(p.offset, p.length);
fDocument.removePosition(fPositionCategory, p);
++first;
}
// remove any 'stacked' partitions, they will be re-added
else if (HereDocUtils.isHereDocContent(contentType)) {
rememberRegion(p.offset, p.length);
fDocument.removePosition(fPositionCategory, p);
++first;
} else
break;
}
// if position already exists and we have scanned at least the
// area covered by the event, we are done
if (fDocument
.containsPosition(fPositionCategory, start, length)) {
if (lastScannedPosition >= (e.getOffset() + newLength))
return createRegion();
++first;
} else {
// insert the new type position
try {
fDocument.addPosition(fPositionCategory,
new TypedPosition(start, length, contentType));
rememberRegion(start, length);
} catch (BadPositionCategoryException x) {
// should never happen on connected documents
} catch (BadLocationException x) {
// should never happen on connected documents
}
}
token = fScanner.nextToken();
}
first = fDocument.computeIndexInCategory(fPositionCategory,
behindLastScannedPosition);
clearPositionCache();
category = getPositions();
TypedPosition p;
while (first < category.length) {
p = (TypedPosition) category[first++];
fDocument.removePosition(fPositionCategory, p);
rememberRegion(p.offset, p.length);
}
} catch (BadPositionCategoryException x) {
// should never happen on connected documents
} catch (BadLocationException x) {
// should never happen on connected documents
} finally {
clearPositionCache();
}
return createRegion();
}
private int findReparseStartForHereDoc(Position[] category, int first,
TypedPosition partition) throws BadLocationException {
if (HereDocUtils.isIdentifier(partition.getType())) {
int hdLine = fDocument.getLineOfOffset(partition.getOffset());
return findReparseStartForIdent(hdLine, category, first);
}
return findReparseStartForTerm(category, first, partition.getType());
}
/*
* b/c heredoc tokens can be 'stacked', find the 'identifier' partition for
* the specified 'terminator' partition and then search for any additional
* 'identifier' paritions that may come before it on the same line
*/
private int findReparseStartForTerm(Position[] positions, int index,
String termContentType) throws BadLocationException {
TypedPosition ident = null;
ListIterator<Position> iter = Arrays.asList(positions).listIterator(
index);
while (iter.hasPrevious()) {
ident = (TypedPosition) iter.previous();
if (HereDocUtils.isIdentForTerm(termContentType, ident.getType())) {
index = iter.nextIndex() + 1;
break;
}
}
// this should never happen as there should always be an opening
// parition above us
Assert.isNotNull(ident,
"unable to find line of a starting heredoc partition");
@SuppressWarnings("null")
int hdLine = fDocument.getLineOfOffset(ident.getOffset());
return findReparseStartForIdent(hdLine, positions, index);
}
/*
* b/c heredoc tokens can be 'stacked', find the first heredoc identifier in
* the list of positions that falls on the same line
*/
private int findReparseStartForIdent(int hdLine, Position[] positions,
int index) throws BadLocationException {
int reparseStart = 0;
ListIterator<Position> iter = Arrays.asList(positions).listIterator(
index);
TypedPosition pos = null;
int pLine;
do {
pos = (TypedPosition) iter.previous();
pLine = fDocument.getLineOfOffset(pos.getOffset());
if (HereDocUtils.isIdentifier(pos.getType())) {
reparseStart = pos.getOffset();
}
} while (hdLine == pLine && iter.hasPrevious());
return reparseStart;
}
@Override protected void initialize() {
// annoying, why isn't this exposed by the parent in some other manner?
fIsInitialized = true;
super.initialize();
}
@Override protected boolean isSupportedContentType(String contentType) {
if (HereDocUtils.isHereDocContent(contentType)) {
return true;
}
return super.isSupportedContentType(contentType);
}
// why isn't this protected/final?
private IRegion createRegion() {
if (fDeleteOffset == -1) {
if ((fStartOffset == -1) || (fEndOffset == -1))
return null;
return new Region(fStartOffset, fEndOffset - fStartOffset);
} else if ((fStartOffset == -1) || (fEndOffset == -1)) {
return new Region(fDeleteOffset, 0);
} else {
int offset = Math.min(fDeleteOffset, fStartOffset);
int endOffset = Math.max(fDeleteOffset, fEndOffset);
return new Region(offset, endOffset - offset);
}
}
// why isn't this protected/final?
private void rememberDeletedOffset(int offset) {
fDeleteOffset = offset;
}
// why isn't this protected/final?
private void rememberRegion(int offset, int length) {
// remember start offset
if (fStartOffset == -1)
fStartOffset = offset;
else if (offset < fStartOffset)
fStartOffset = offset;
// remember end offset
int endOffset = offset + length;
if (fEndOffset == -1)
fEndOffset = endOffset;
else if (endOffset > fEndOffset)
fEndOffset = endOffset;
}
}