/*******************************************************************************
* Copyright 2005-2007, CHISEL Group, University of Victoria, Victoria, BC, Canada
* and IBM Corporation. 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:
* The Chisel Group, University of Victoria
*******************************************************************************/
package net.sourceforge.tagsea.parsed.parser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import net.sourceforge.tagsea.core.IWaypoint;
import net.sourceforge.tagsea.parsed.ParsedWaypointPlugin;
import net.sourceforge.tagsea.parsed.core.IParsedWaypointDefinition;
import net.sourceforge.tagsea.parsed.core.ParsedWaypointUtils;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.IFileBuffer;
import org.eclipse.core.filebuffers.IFileBufferListener;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension3;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.TypedRegion;
import org.eclipse.jface.text.rules.EndOfLineRule;
import org.eclipse.jface.text.rules.FastPartitioner;
import org.eclipse.jface.text.rules.IPartitionTokenScanner;
import org.eclipse.jface.text.rules.IPredicateRule;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.MultiLineRule;
import org.eclipse.jface.text.rules.RuleBasedPartitionScanner;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
/**
* A parser that can be used for source code or text files that have natural single and multi-line
* comments.
* @author Del Myers
*
*/
public abstract class BasicCommentParser implements IWaypointParser, IReconcilingWaypointParser {
public static final int MULTILINE = 2;
public static final int SINGLELINE = 1;
public static final int UNKNOWN = 0;
//the scanner used to discover comments.
private RuleBasedPartitionScanner commentScanner;
private final String LINE_CONTENT = "__wpnt_single_line";
private final IToken LINE_TOKEN = new Token(LINE_CONTENT);
private final String MULTI_LINE_CONTENT = "__wpnt_multi_line";
private final IToken MULTILINE_TOKEN = new Token(MULTI_LINE_CONTENT);
private static final String EXCLUSION_CONTENT = "__wpnt_exclusion";
private static final IToken EXCLUSION_TOKEN = new Token(EXCLUSION_CONTENT);
private final String PARTITION_SUFFIX = ".partition";
private String partitioning;
private HashMap<IDocument, IDocumentPartitioner> partionerMap;
private String[] singleLineStarts;
private String[] multiLineStarts;
private String[] multiLineEnds;
/**
* Constructs a new comment parser using the given strings to discover where comment regions are.
* Multi-line comments are indicated using start/end string pairs. multiLineStart and multiLineEnd
* represent those pairs where multiLineStart[x] is the start indicator for the multi-line comment
* terminated by multiLineEnd[x]. Hence multiLineStart and multiLineEnd must have equal length.
* The lists may not have length 0, cannot be null, nor may they have any null elements or empty
* strings.
* @param singleLine strings indicating the start of a single-line comment.
* @param multiLineStart strings indicating the start of a multi-line comment.
* @param multiLineEnd strings indicating the end of a multi-line comment.
* @param exclusionStart strings that indicate the beginnings of regions that should be ignored, such as strings.
* @param exclusionEnd strings taht indication the ends of regions that should be ignored, such as strings. Must be the same length as exclusionStart.
*/
public BasicCommentParser(String[] singleLine, String[] multiLineStart, String[] multiLineEnd, String[] exclusionStart, String[] exclusionEnd) {
if (multiLineStart.length != multiLineEnd.length) {
throw new IllegalArgumentException("Unmatched start/end pair");
} else if (multiLineStart.length == 0 && singleLine.length == 0) {
throw new IllegalArgumentException("No comment markers defined");
}
checkArray(singleLine);
checkArray(multiLineStart);
checkArray(multiLineEnd);
if (exclusionStart != null || exclusionEnd != null) {
//can both be null, but not just one can be null
if (exclusionStart == null || exclusionEnd == null) {
throw new IllegalArgumentException("Null reference");
}
if (exclusionStart.length != exclusionEnd.length) {
throw new IllegalArgumentException("Unmatched start/end pair");
}
checkArray(exclusionStart);
checkArray(exclusionEnd);
}
LinkedList<IPredicateRule> rules = new LinkedList<IPredicateRule>();
for (String s : singleLine) {
rules.add(new EndOfLineRule(s, LINE_TOKEN));
}
for (int i = 0; i < multiLineStart.length; i++) {
rules.add(new MultiLineRule(multiLineStart[i], multiLineEnd[i], MULTILINE_TOKEN, (char)0, true));
}
//add the rules for exclusion
if (exclusionStart != null && exclusionEnd != null) {
for (int i = 0; i < exclusionStart.length; i++) {
rules.add(new MultiLineRule(exclusionStart[i], exclusionEnd[i], EXCLUSION_TOKEN, (char)0, true));
}
}
this.singleLineStarts = singleLine;
this.multiLineStarts = multiLineStart;
this.multiLineEnds = multiLineEnd;
IPredicateRule[] rulesArray = rules.toArray(new IPredicateRule[rules.size()]);
this.commentScanner = new RuleBasedPartitionScanner();
this.commentScanner.setPredicateRules(rulesArray);
this.partionerMap = new HashMap<IDocument, IDocumentPartitioner>();
FileBuffers.getTextFileBufferManager().addFileBufferListener(new IFileBufferListener(){
public void bufferContentAboutToBeReplaced(IFileBuffer buffer) {}
public void bufferContentReplaced(IFileBuffer buffer) {}
public void bufferCreated(IFileBuffer buffer) {
connectBuffer((ITextFileBuffer)buffer);
}
public void bufferDisposed(IFileBuffer buffer) {
if (Display.getCurrent() == null) {
final ITextFileBuffer fBuffer = (ITextFileBuffer) buffer;
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable(){public void run(){disconnectBuffer(fBuffer);}});
} else {
disconnectBuffer((ITextFileBuffer)buffer);
}
}
public void dirtyStateChanged(IFileBuffer buffer, boolean isDirty) {}
public void stateChangeFailed(IFileBuffer buffer) {}
public void stateChanging(IFileBuffer buffer) {}
public void stateValidationChanged(IFileBuffer buffer,
boolean isStateValidated) {}
public void underlyingFileDeleted(IFileBuffer buffer) {}
public void underlyingFileMoved(IFileBuffer buffer, IPath path) {}
});
}
protected boolean isManagedFile(IFile file, IContentType content){
String kind = getParsedWaypointKind();
IParsedWaypointDefinition def =
ParsedWaypointPlugin.getDefault().getParsedWaypointRegistry().getDefinition(kind);
IParsedWaypointDefinition[] defs =
ParsedWaypointPlugin.getDefault().getParsedWaypointRegistry().getMatchingDefinitions(file);
for (IParsedWaypointDefinition check : defs) {
if (check == def) return true;
}
return false;
}
private void connectBuffer(ITextFileBuffer buffer) {
IContentType type = null;
try {
type = buffer.getContentType();
} catch (CoreException e) {
}
IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(buffer.getLocation());
if (resource instanceof IFile) {
if (!isManagedFile((IFile)resource, type)) {
return;
}
}
IDocumentPartitioner partitioner =
new FastPartitioner(((IPartitionTokenScanner)commentScanner), new String[] {LINE_CONTENT, MULTI_LINE_CONTENT});
partionerMap.put(buffer.getDocument(), partitioner);
if (Display.getCurrent() == null) {
final ITextFileBuffer fBuffer = buffer;
PlatformUI.getWorkbench().getDisplay().syncExec(new Runnable(){public void run(){prepareDocument(fBuffer.getDocument());}});
} else {
prepareDocument(buffer.getDocument());
}
}
private void disconnectBuffer(ITextFileBuffer buffer) {
IDocument document = buffer.getDocument();
IDocumentPartitioner partitioner = partionerMap.get(buffer.getDocument());
if (partitioner != null) {
if (document instanceof IDocumentExtension3) {
IDocumentExtension3 d = (IDocumentExtension3) document;
IDocumentPartitioner partitioner2 = d.getDocumentPartitioner(getPartitioning());
if (partitioner == partitioner2) {
partitioner.disconnect();
d.setDocumentPartitioner(getPartitioning(), null);
}
}
}
}
/**
* @param singleLine
*/
private void checkArray(String[] array) {
if (array == null) {
throw new IllegalArgumentException("Null argument");
} else {
for (String s : array) {
if (s == null) {
throw new IllegalArgumentException("Null element");
} else if ("".equals(s)) {
throw new IllegalArgumentException("Empty element");
}
}
}
}
/**
* Parses the document for waypoint descriptors. The regions are expected to be either:
* <ol>
* <li>Regions representing valid comments in the document</li>
* <li>A one-element array with a region that spans the entire document</li>
* </ol>
* Clients should rarely need to call this method as it will be called automatically within
* the framework. If clients need to use it, however, it is recommended that they parse the
* entire document rather than trying to calculate the individual comment regions first as
* the latter is more error prone.
*/
public final IParsedWaypointDescriptor[] parse(final IDocument document, IRegion[] regions, IWaypointParseProblemCollector collector) {
//see if we need to check the entire document.
if (regions.length == 1) {
if (regions[0].getOffset() == 0 && regions[0].getLength() == document.getLength()) {
regions = gatherCommentRegions(document, 0, document.getLength());
}
}
List<IParsedWaypointDescriptor> descriptors = new LinkedList<IParsedWaypointDescriptor>();
for (IRegion region : regions) {
//we can parse for comments because we know that according to the contract of
//IReconcilingWaypointParser, the regions will always be either the entire document,
//or the regions defined by calculatedirtyRegions() (unless a third-party uses the
//parser, which he/she does at his/her own risk).
descriptors.addAll(doParseInComment(document, region, collector));
}
return descriptors.toArray(new IParsedWaypointDescriptor[descriptors.size()]);
}
/**
* Parses the comment within the given region to look for a waypoint. It can be assumed that
* the text within the given region represents a valid comment block.
* @param document the document to parse.
* @param region the comment region.
* @return a list of ParsedWaypointDescriptors that represent waypoints or parse errors.
*/
protected abstract List<IParsedWaypointDescriptor> doParseInComment(IDocument document, IRegion region, IWaypointParseProblemCollector collector);
/**
* Returns the unique id for the kind of waypoint that this parser is used to discover.
* @return the unique id for the kind of waypoint that this parser is used to discover.
*/
public abstract String getParsedWaypointKind();
/**
* Prepares the document for parsing or reconciling.
* @param document
*/
private void prepareDocument(IDocument document) {
//attempts to use a document partitioner to quickly and always keep the
//comment regions up-to-date.
if (document instanceof IDocumentExtension3) {
IDocumentPartitioner documentPartitioner = partionerMap.get(document);
if (documentPartitioner == null) {
ITextFileBuffer buffer =
ParsedWaypointPlugin.getDefault().getPlatformDocumentRegistry().getBufferForDocument(document);
if (buffer != null) {
connectBuffer(buffer);
documentPartitioner = partionerMap.get(document);
if (documentPartitioner == null)
return;
}
}
IDocumentExtension3 d = (IDocumentExtension3) document;
IDocumentPartitioner partitioner = d.getDocumentPartitioner(getPartitioning());
if (partitioner == null) {
d.setDocumentPartitioner(getPartitioning(), documentPartitioner);
documentPartitioner.connect(document);
}
}
}
public IRegion[] calculateDirtyRegions(final DocumentEvent event) {
final IDocument doc = event.getDocument();
final IRegion runnableResult[][] = new IRegion[1][];
Runnable displayRunnable = new Runnable(){
public void run() {
//get the regions that intersect with the dirty portion of the event.
if (doc instanceof IDocumentExtension3) {
if (((IDocumentExtension3)doc).getDocumentPartitioner(partitioning) == null) {
runnableResult[0] = gatherCommentRegions(event.getDocument(), 0, event.getDocument().getLength());
}
int textLength = (event.getText() != null) ? event.getText().length() : 0;
Region eventRegion = new Region(event.getOffset(), textLength);
ITypedRegion[] regions;
try {
regions = TextUtilities.computePartitioning(
doc,
partitioning,
0,
event.getDocument().getLength(),
true);
LinkedList<IRegion> result = new LinkedList<IRegion>();
if (regions.length == 0) {
runnableResult[0] = new IRegion[] {new Region(0, doc.getLength())};
return;
}
for (IRegion region : regions) {
if (!IDocument.DEFAULT_CONTENT_TYPE.equals(((TypedRegion)region).getType()) &&
TextUtilities.overlaps(region, eventRegion)) {
result.add(region);
}
}
runnableResult[0] = result.toArray(new IRegion[result.size()]);
return;
} catch (BadLocationException e) {
ParsedWaypointPlugin.getDefault().log(e);
} catch (RuntimeException e) {
ITextFileBuffer buffer =
ParsedWaypointPlugin.getDefault().getPlatformDocumentRegistry().getBufferForDocument(doc);
if (buffer != null) {
disconnectBuffer(buffer);
}
}
}
runnableResult[0] = gatherCommentRegions(event.getDocument(), 0, event.getDocument().getLength());
}
};
if (Display.getCurrent() != null) {
displayRunnable.run();
} else {
PlatformUI.getWorkbench().getDisplay().syncExec(displayRunnable);
}
if (runnableResult[0] == null) {
}
return runnableResult[0];
}
/**
* Used in cases where partitioning can't be done on the document. Scans the entire document for
* comment areas.
* @return
*/
public IRegion[] gatherCommentRegions(IDocument document, int offset, int length) {
List<IRegion> commentRegions = new ArrayList<IRegion>();
commentScanner.setRange(document,offset, length);
while (true)
{
IToken token = commentScanner.nextToken();
if (token.isEOF()) {
break;
} else if (token.getData() == LINE_CONTENT || token.getData() == MULTI_LINE_CONTENT) {
commentRegions.add(new Region(commentScanner.getTokenOffset(), commentScanner.getTokenLength()));
}
}
IRegion[] result= new IRegion[commentRegions.size()];
commentRegions.toArray(result);
return result;
}
private String getPartitioning() {
if (this.partitioning == null) {
partitioning = getParsedWaypointKind() + PARTITION_SUFFIX;
}
return partitioning;
}
/**
* Returns the strings that indicate the beginning of a single-line comment. This is a handle-only method.
* @return the strings that indicate the beginning of a single-line comment.
*/
public String[] getSingleLineIndicators() {
String[] result = new String[singleLineStarts.length];
System.arraycopy(singleLineStarts, 0, result, 0, result.length);
return result;
}
/**
* Returns the strings that indicate the beginning of a multi-line comment. This is a handle-only method.
* @return the strings that indicate the beginning of a multi-line comment.
*/
public String[] getMultilineStartIndicators() {
String[] result = new String[multiLineStarts.length];
System.arraycopy(multiLineStarts, 0, result, 0, result.length);
return result;
}
/**
* Returns the strings that indicate the end of a multi-line comment. This is a handle-only method.
* @return the strings that indicate the end of a multi-line comment.
*/
public String[] getMultilineEndIndicators() {
String[] result = new String[multiLineEnds.length];
System.arraycopy(multiLineEnds, 0, result, 0, result.length);
return result;
}
/**
* Returns the comment type for the given waypoint. One of MULTILINE, SINGLELINE, or UNKNOWN.
* @param wp the waypoint to check.
* @return the comment type for the given waypoint. One of MULTILINE, SINGLELINE, or UNKNOWN.
*/
public int getCommentTypeFor(final IWaypoint wp) {
final int[] runnableResult = new int[1];
Runnable displayRunnable = new Runnable() {
public void run() {
IRegion wpRegion = new Region(ParsedWaypointUtils.getCharStart(wp), ParsedWaypointUtils.getLength(wp));
IDocument doc = ParsedWaypointUtils.getDocument(wp);
if (doc instanceof IDocumentExtension3 && partionerMap.keySet().contains(doc)) {
if (((IDocumentExtension3)doc).getDocumentPartitioner(partitioning) == null) {
runnableResult[0] = UNKNOWN;
return;
}
ITypedRegion[] regions;
try {
regions = TextUtilities.computePartitioning(
doc,
partitioning,
0,
doc.getLength(),
true);
LinkedList<IRegion> result = new LinkedList<IRegion>();
for (ITypedRegion region : regions) {
if (TextUtilities.overlaps(region, wpRegion)) {
if (region.getType().equals(MULTI_LINE_CONTENT)) {
runnableResult[0]=MULTILINE;
return;
} else if (region.getType().equals(LINE_CONTENT)) {
runnableResult[0]=SINGLELINE;
return;
}
}
if (!IDocument.DEFAULT_CONTENT_TYPE.equals(((TypedRegion)region).getType()) &&
TextUtilities.overlaps(region, wpRegion)) {
result.add(region);
}
}
} catch (BadLocationException e) {
}
}
runnableResult[0] = UNKNOWN;
}
};
if (Display.getCurrent() != null) {
displayRunnable.run();
} else {
PlatformUI.getWorkbench().getDisplay().syncExec(displayRunnable);
}
return runnableResult[0];
}
}