/*******************************************************************************
* Copyright (c) 2007, 2013 David Green and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* David Green - initial API and implementation
*******************************************************************************/
package org.eclipse.mylyn.internal.wikitext.ui.editor.syntax;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.rules.FastPartitioner;
import org.eclipse.jface.text.rules.IPartitionTokenScanner;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.mylyn.wikitext.parser.Attributes;
import org.eclipse.mylyn.wikitext.parser.DocumentBuilder;
import org.eclipse.mylyn.wikitext.parser.Locator;
import org.eclipse.mylyn.wikitext.parser.MarkupParser;
import org.eclipse.mylyn.wikitext.parser.markup.AbstractMarkupLanguage;
import org.eclipse.mylyn.wikitext.parser.markup.MarkupLanguage;
import org.eclipse.osgi.util.NLS;
/**
* @author David Green
*/
public class FastMarkupPartitioner extends FastPartitioner {
public static final String CONTENT_TYPE_MARKUP = "__markup_block"; //$NON-NLS-1$
public static final String[] ALL_CONTENT_TYPES = new String[] { CONTENT_TYPE_MARKUP };
static boolean debug = Boolean.getBoolean(FastMarkupPartitioner.class.getName() + ".debug"); //$NON-NLS-1$
private MarkupLanguage markupLanguage;
public FastMarkupPartitioner() {
super(new PartitionTokenScanner(), ALL_CONTENT_TYPES);
}
public MarkupLanguage getMarkupLanguage() {
return markupLanguage;
}
public void setMarkupLanguage(MarkupLanguage markupLanguage) {
this.markupLanguage = markupLanguage;
getScanner().setMarkupLanguage(markupLanguage);
resetPartitions();
}
PartitionTokenScanner getScanner() {
return (PartitionTokenScanner) fScanner;
}
public void resetPartitions() {
if (fDocument != null) {
super.flushRewriteSession();
initialize();
} else {
clearPositionCache();
}
}
static class PartitionTokenScanner implements IPartitionTokenScanner {
private final Map<Integer, PartitioningResult> cachedPartitioning = new HashMap<>();
private MarkupLanguage markupLanguage;
private int index = -1;
private PartitioningResult lastComputed;
private static class PartitioningResult {
int offset;
int length;
ITypedRegion[] partitions;
public PartitioningResult(int offset, int length, ITypedRegion[] partitions) {
super();
this.offset = offset;
this.length = length;
this.partitions = partitions;
}
public boolean overlapsWith(PartitioningResult other) {
int end = other.offset + other.length;
int thisEnd = this.offset + this.length;
if (end > thisEnd) {
return other.offset < thisEnd;
} else if (thisEnd > end) {
return offset < end;
} else {
return true;
}
}
}
public ITypedRegion[] computePartitions(IDocument document, int offset, int length) {
if (lastComputed != null && lastComputed.offset <= offset
&& (lastComputed.offset + lastComputed.length) >= (offset + length)) {
return lastComputed.partitions;
} else {
PartitioningResult result = cachedPartitioning.get(offset);
if (result == null || result.length != length) {
result = computeOlp(document, offset, length, -1);
updateCache(result, document.getLength());
}
return result.partitions;
}
}
public void setPartialRange(IDocument document, int offset, int length, String contentType, int partitionOffset) {
lastComputed = computeOlp(document, offset, length, partitionOffset);
index = -1;
updateCache(lastComputed, document.getLength());
}
private void updateCache(PartitioningResult updated, int maxLength) {
Iterator<PartitioningResult> it = cachedPartitioning.values().iterator();
while (it.hasNext()) {
PartitioningResult result = it.next();
if (result.offset >= maxLength || (updated != null && result.overlapsWith(updated))) {
it.remove();
}
}
if (updated != null) {
cachedPartitioning.put(updated.offset, updated);
}
}
private PartitioningResult computeOlp(IDocument document, int offset, int length, int partitionOffset) {
if (markupLanguage == null) {
return new PartitioningResult(offset, length, null);
}
int startOffset = partitionOffset == -1 ? offset : Math.min(offset, partitionOffset);
int endOffset = offset + length;
boolean blocksOnly = partitionOffset != -1;
MarkupParser markupParser = new MarkupParser(markupLanguage);
if (markupLanguage instanceof AbstractMarkupLanguage) {
AbstractMarkupLanguage language = (AbstractMarkupLanguage) markupLanguage;
language.setFilterGenerativeContents(true);
language.setBlocksOnly(blocksOnly);
}
PartitionBuilder partitionBuilder = new PartitionBuilder(startOffset, blocksOnly);
markupParser.setBuilder(partitionBuilder);
String markupContent;
try {
markupContent = document.get(startOffset, endOffset - startOffset);
} catch (BadLocationException e) {
markupContent = document.get();
}
markupParser.parse(markupContent);
ITypedRegion[] latestPartitions = partitionBuilder.partitions.toArray(new ITypedRegion[partitionBuilder.partitions.size()]);
List<ITypedRegion> partitioning = new ArrayList<>(latestPartitions.length);
ITypedRegion previous = null;
for (ITypedRegion region : latestPartitions) {
if (region.getLength() == 0) {
// ignore 0-length partitions
continue;
}
if (previous != null && region.getOffset() < (previous.getOffset() + previous.getLength())) {
String message = NLS.bind(Messages.FastMarkupPartitioner_0, new Object[] { region, previous,
markupLanguage.getName() });
if (FastMarkupPartitioner.debug) {
String markupSavePath = saveToTempFile(markupLanguage, markupContent);
message = NLS.bind(Messages.FastMarkupPartitioner_1, new Object[] { message, markupSavePath });
}
throw new IllegalStateException(message);
}
previous = region;
if (region.getOffset() >= startOffset && region.getOffset() < endOffset) {
partitioning.add(region);
} else if (region.getOffset() >= (offset + length)) {
break;
}
}
return new PartitioningResult(offset, length, partitioning.toArray(new ITypedRegion[partitioning.size()]));
}
public void setMarkupLanguage(MarkupLanguage markupLanguage) {
this.markupLanguage = markupLanguage;
}
public int getTokenLength() {
return lastComputed.partitions[index].getLength();
}
public int getTokenOffset() {
return lastComputed.partitions[index].getOffset();
}
public IToken nextToken() {
if (lastComputed == null || lastComputed.partitions == null || ++index >= lastComputed.partitions.length) {
return Token.EOF;
}
return new Token(lastComputed.partitions[index].getType());
}
public void setRange(IDocument document, int offset, int length) {
setPartialRange(document, offset, length, null, -1);
}
}
public static class MarkupPartition implements ITypedRegion {
private final Block block;
private int offset;
private int length;
private List<Span> spans;
private MarkupPartition(Block block, int offset, int length) {
this.block = block;
this.offset = offset;
this.length = length;
}
public int getOffset() {
return offset;
}
public int getLength() {
return length;
}
public String getType() {
return CONTENT_TYPE_MARKUP;
}
public Block getBlock() {
return block;
}
public List<Span> getSpans() {
if (spans == null) {
List<Span> spans = new ArrayList<>();
getSpans(block, spans);
this.spans = spans;
}
return spans;
}
private void getSpans(Block block, List<Span> spans) {
for (Segment<?> s : block.getChildren().asList()) {
if (s.getOffset() >= offset && s.getOffset() < (offset + length)) {
if (s instanceof Span) {
spans.add((Span) s);
} else {
getSpans((Block) s, spans);
}
}
}
}
@Override
public String toString() {
return String.format("MarkupPartition(type=%s,offset=%s,length=%s,end=%s)", block.getType(), offset, //$NON-NLS-1$
length, offset + length);
}
}
private static class PartitionBuilder extends DocumentBuilder {
private final Block outerBlock = new Block(null, 0, Integer.MAX_VALUE / 2);
private Block currentBlock = outerBlock;
private Span currentSpan = null;
private final int offset;
private List<MarkupPartition> partitions;
private final boolean blocksOnly;
public PartitionBuilder(int offset, boolean blocksOnly) {
this.offset = offset;
this.blocksOnly = blocksOnly;
}
@Override
public void acronym(String text, String definition) {
}
@Override
public void beginBlock(BlockType type, Attributes attributes) {
final int newBlockOffset = getLocator().getDocumentOffset() + offset;
Block newBlock = new Block(type, attributes, newBlockOffset, currentBlock.getLength()
- (newBlockOffset - currentBlock.getOffset()));
newBlock.setSpansComputed(!blocksOnly);
currentBlock.add(newBlock);
currentBlock = newBlock;
}
@Override
public void beginDocument() {
}
@Override
public void beginHeading(int level, Attributes attributes) {
final int newBlockOffset = getLocator().getDocumentOffset() + offset;
Block newBlock = new Block(level, attributes, newBlockOffset, currentBlock.getLength()
- (newBlockOffset - currentBlock.getOffset()));
newBlock.setSpansComputed(!blocksOnly);
currentBlock.add(newBlock);
currentBlock = newBlock;
}
@Override
public void beginSpan(SpanType type, Attributes attributes) {
Span span = new Span(type, attributes, getLocator().getDocumentOffset() + offset,
getLocator().getLineSegmentEndOffset() - getLocator().getLineCharacterOffset());
if (currentSpan != null) {
currentSpan.add(span);
currentSpan = span;
} else {
currentSpan = span;
currentBlock.add(span);
}
}
@Override
public void characters(String text) {
}
@Override
public void charactersUnescaped(String literal) {
}
@Override
public void endBlock() {
currentBlock = currentBlock.getParent();
if (currentBlock == null) {
throw new IllegalStateException();
}
}
@Override
public void endDocument() {
if (currentBlock != outerBlock) {
throw new IllegalStateException();
}
Locator locator = getLocator();
outerBlock.setLength((locator == null ? 0 : locator.getDocumentOffset()) + offset);
partitions = new ArrayList<>();
// here we flatten our hierarchy of blocks into partitions
for (Segment<?> child : outerBlock.getChildren().asList()) {
createRegions(null, child);
}
}
public MarkupPartition createRegions(MarkupPartition parent, Segment<?> segment) {
if (segment.getLength() == 0) {
return parent;
}
if (segment instanceof Block) {
Block block = (Block) segment;
if (!filtered(block)) {
MarkupPartition partition = new MarkupPartition(block, segment.getOffset(), segment.getLength());
if (parent == null) {
partitions.add(partition);
} else {
// parent needs adjusting to prevent overlap
int parentIndex = partitions.indexOf(parent);
// start on the same offset
if (partition.offset == parent.offset) {
if (partition.length == parent.length) {
// same length, so remove parent all together
partitions.remove(parentIndex);
partitions.add(parentIndex, partition);
} else {
// start on same offset, but new partition is smaller
// so move parent after new partition and shrink it by the corresponding amount
parent.offset = partition.offset + partition.length;
parent.length -= partition.length;
partitions.add(parentIndex, partition);
}
} else {
if (partition.length + partition.offset == parent.length + parent.offset) {
// end on the same offset, so shrink the parent
parent.length = partition.offset - parent.offset;
partitions.add(parentIndex + 1, partition);
} else {
// split the parent
int parentLength = parent.length;
parent.length = partition.offset - parent.offset;
final int splitOffset = partition.offset + partition.length;
MarkupPartition trailer = new MarkupPartition(parent.block, splitOffset, parent.offset
+ parentLength - splitOffset);
partitions.add(parentIndex + 1, partition);
partitions.add(parentIndex + 2, trailer);
parent = trailer;
}
}
}
if (!block.getChildren().isEmpty()) {
for (Segment<?> child : block.getChildren().asList()) {
partition = createRegions(partition, child);
}
}
}
}
return parent;
}
private boolean filtered(Block block) {
if (block.getType() == null) {
return false;
}
switch (block.getType()) {
case DEFINITION_ITEM:
case LIST_ITEM:
case TABLE_CELL_HEADER:
case TABLE_CELL_NORMAL:
case TABLE_ROW:
return true;
case PARAGRAPH:
// bug 249615: ignore paras that are nested inside a quote block
if (block.getParent() != null && block.getParent().getType() == BlockType.QUOTE) {
return true;
}
break;
case BULLETED_LIST:
case NUMERIC_LIST:
case DEFINITION_LIST:
case DEFINITION_TERM:
return block.getParent() != null && filtered(block.getParent());
}
return false;
}
@Override
public void endHeading() {
currentBlock = currentBlock.getParent();
if (currentBlock == null) {
throw new IllegalStateException();
}
}
@Override
public void endSpan() {
if (currentSpan == null) {
throw new IllegalStateException();
}
if (currentSpan.getParent() instanceof Span) {
currentSpan = (Span) currentSpan.getParent();
} else {
currentSpan = null;
}
}
@Override
public void entityReference(String entity) {
}
@Override
public void image(Attributes attributes, String url) {
}
@Override
public void imageLink(Attributes linkAttributes, Attributes attributes, String href, String imageUrl) {
}
@Override
public void lineBreak() {
}
@Override
public void link(Attributes attributes, String hrefOrHashName, String text) {
}
}
public void reparse(IDocument document, Block block) {
MarkupParser markupParser = new MarkupParser(markupLanguage);
if (markupLanguage instanceof AbstractMarkupLanguage) {
AbstractMarkupLanguage language = (AbstractMarkupLanguage) markupLanguage;
language.setFilterGenerativeContents(true);
language.setBlocksOnly(false);
}
PartitionBuilder partitionBuilder = new PartitionBuilder(block.getOffset(), false);
markupParser.setBuilder(partitionBuilder);
try {
markupParser.parse(document.get(block.getOffset(), block.getLength()));
for (Segment<?> s : partitionBuilder.outerBlock.getChildren().asList()) {
if (s.getOffset() == block.getOffset()) {
if (s instanceof Block) {
block.replaceChildren(s);
block.setSpansComputed(true);
break;
}
}
}
} catch (BadLocationException e) {
throw new IllegalStateException(e);
}
}
/**
* save markup content to a temporary file to facilitate analysis of the problem
*
* @return the absolute path to the saved file, or null if the file was not saved
*/
private static String saveToTempFile(MarkupLanguage markupLanguage, String markupContent) {
String markupSavePath = null;
try {
File file = File.createTempFile("markup-content-", "." //$NON-NLS-1$ //$NON-NLS-2$
+ markupLanguage.getName().toLowerCase().replaceAll("[^a-z]", "")); //$NON-NLS-1$ //$NON-NLS-2$
Writer writer = new FileWriter(file);
try {
writer.write(markupContent);
} finally {
writer.close();
}
markupSavePath = file.getAbsolutePath();
} catch (IOException e) {
}
return markupSavePath;
}
}