blob: 14e9b1532bababc1cc1d0c280919f9facb76bdcf [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2001, 2010 IBM Corporation 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:
* IBM Corporation - initial API and implementation
* Jens Lukowski/Innoopract - initial renaming/restructuring
*
*******************************************************************************/
package org.eclipse.wst.sse.core.internal.text.rules;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.wst.sse.core.internal.ltk.parser.IBlockedStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.parser.ForeignRegion;
import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentRegionsReplacedEvent;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredTextPartitioner;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionCollection;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionContainer;
import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList;
import org.eclipse.wst.sse.core.text.IStructuredPartitions;
/**
* Base Document partitioner for StructuredDocuments. BLOCK_TEXT ITextRegions
* have a partition type of BLOCK or BLOCK:TAGNAME if a surrounding tagname
* was recorded.
*
* Subclasses should synchronize access to <code>internalReusedTempInstance</code> using the lock
* <code>PARTITION_LOCK</code>.
*/
public class StructuredTextPartitioner implements IDocumentPartitioner, IStructuredTextPartitioner {
static class CachedComputedPartitions {
int fLength;
int fOffset;
ITypedRegion[] fPartitions;
boolean isInValid;
CachedComputedPartitions(int offset, int length, ITypedRegion[] partitions) {
fOffset = offset;
fLength = length;
fPartitions = partitions;
isInValid = true;
}
}
private CachedComputedPartitions cachedPartitions = new CachedComputedPartitions(-1, -1, null);
protected String[] fSupportedTypes = null;
protected IStructuredTypedRegion internalReusedTempInstance = new SimpleStructuredTypedRegion(0, 0, IStructuredPartitions.DEFAULT_PARTITION);
protected IStructuredDocument fStructuredDocument;
protected final Object PARTITION_LOCK = new Object();
/**
* StructuredTextPartitioner constructor comment.
*/
public StructuredTextPartitioner() {
super();
}
/**
* Returns the partitioning of the given range of the connected document.
* There must be a document connected to this partitioner.
*
* Note: this shouldn't be called directly by clients, unless they control
* the threading that includes modifications to the document. Otherwise
* the document could be modified while partitions are being computed. We
* advise that clients use the computePartitions API directly from the
* document, so they won't have to worry about that.
*
* @param offset
* the offset of the range of interest
* @param length
* the length of the range of interest
* @return the partitioning of the range
*/
public ITypedRegion[] computePartitioning(int offset, int length) {
if (fStructuredDocument == null) {
throw new IllegalStateException("document partitioner is not connected"); //$NON-NLS-1$
}
ITypedRegion[] results = null;
synchronized (cachedPartitions) {
if ((!cachedPartitions.isInValid) && (offset == cachedPartitions.fOffset) && (length == cachedPartitions.fLength))
results = cachedPartitions.fPartitions;
}
if (results == null) {
if (length == 0) {
results = new ITypedRegion[]{getPartition(offset)};
} else {
List list = new ArrayList();
int endPos = offset + length;
if (endPos > fStructuredDocument.getLength()) {
// This can occur if the model instance is being
// changed
// and everyone's not yet up to date
return new ITypedRegion[]{createPartition(offset, length, getUnknown())};
}
int currentPos = offset;
IStructuredTypedRegion previousPartition = null;
while (currentPos < endPos) {
IStructuredTypedRegion partition = null;
synchronized (PARTITION_LOCK) {
internalGetPartition(currentPos, false);
currentPos += internalReusedTempInstance.getLength();
// check if this partition just continues last one
// (type is the same),
// if so, just extend length of last one, not need to
// create new
// instance.
if (previousPartition != null && internalReusedTempInstance.getType().equals(previousPartition.getType())) {
// same partition type
previousPartition.setLength(previousPartition.getLength() + internalReusedTempInstance.getLength());
}
else {
// not the same, so add to list
partition = createNewPartitionInstance();
}
}
if (partition != null) {
list.add(partition);
// and make current, previous
previousPartition = partition;
}
}
results = new ITypedRegion[list.size()];
list.toArray(results);
}
if (results.length > 0) {
// truncate returned results to requested range
if (results[0].getOffset() < offset && results[0] instanceof IStructuredRegion) {
((IStructuredRegion) results[0]).setOffset(offset);
}
int lastEnd = results[results.length - 1].getOffset() + results[results.length - 1].getLength();
if (lastEnd > offset + length && results[results.length - 1] instanceof IStructuredRegion) {
((IStructuredRegion) results[results.length - 1]).setLength(offset + length - results[results.length - 1].getOffset());
}
}
synchronized (cachedPartitions) {
cachedPartitions.fLength = length;
cachedPartitions.fOffset = offset;
cachedPartitions.fPartitions = results;
cachedPartitions.isInValid = false;
}
}
return results;
}
private void invalidatePartitionCache() {
synchronized (cachedPartitions) {
cachedPartitions.isInValid = true;
}
}
/**
* Connects the document to the partitioner, i.e. indicates the begin of
* the usage of the receiver as partitioner of the given document.
*/
public synchronized void connect(IDocument document) {
if (document instanceof IStructuredDocument) {
invalidatePartitionCache();
this.fStructuredDocument = (IStructuredDocument) document;
} else {
throw new IllegalArgumentException("This class and API are for Structured Documents only"); //$NON-NLS-1$
}
}
/**
* Determines if the given ITextRegionContainer itself contains another
* ITextRegionContainer
*
* @param ITextRegionContainer
* @return boolean
*/
protected boolean containsEmbeddedRegion(IStructuredDocumentRegion container) {
boolean containsEmbeddedRegion = false;
ITextRegionList regions = container.getRegions();
for (int i = 0; i < regions.size(); i++) {
ITextRegion region = regions.get(i);
if (region instanceof ITextRegionContainer) {
containsEmbeddedRegion = true;
break;
}
}
return containsEmbeddedRegion;
}
private IStructuredTypedRegion createNewPartitionInstance() {
synchronized (PARTITION_LOCK) {
return new SimpleStructuredTypedRegion(internalReusedTempInstance.getOffset(), internalReusedTempInstance.getLength(), internalReusedTempInstance.getType());
}
}
/**
* Creates the concrete partition from the given values. Returns a new
* instance for each call.
*
* Subclasses may override.
*
* @param offset
* @param length
* @param type
* @return ITypedRegion
*
* TODO: should be protected
*/
public IStructuredTypedRegion createPartition(int offset, int length, String type) {
return new SimpleStructuredTypedRegion(offset, length, type);
}
/**
* Disconnects the document from the partitioner, i.e. indicates the end
* of the usage of the receiver as partitioner of the given document.
*
* @see org.eclipse.jface.text.IDocumentPartitioner#disconnect()
*/
public synchronized void disconnect() {
invalidatePartitionCache();
this.fStructuredDocument = null;
}
/**
* Informs about a forthcoming document change.
*
* @see org.eclipse.jface.text.IDocumentPartitioner#documentAboutToBeChanged(DocumentEvent)
*/
public void documentAboutToBeChanged(DocumentEvent event) {
invalidatePartitionCache();
}
/**
* The document has been changed. The partitioner updates the set of
* regions and returns whether the structure of the document partitioning
* has been changed, i.e. whether partitions have been added or removed.
*
* @see org.eclipse.jface.text.IDocumentPartitioner#documentChanged(DocumentEvent)
*/
public boolean documentChanged(DocumentEvent event) {
boolean result = false;
if (event instanceof StructuredDocumentRegionsReplacedEvent) {
// partitions don't always change while document regions do,
// but that's the only "quick check" we have.
// I'm not sure if something more sophisticated will be needed
// in the future. (dmw, 02/18/04).
result = true;
}
return result;
}
protected boolean doParserSpecificCheck(int offset, boolean partitionFound, IStructuredDocumentRegion sdRegion, IStructuredDocumentRegion previousStructuredDocumentRegion, ITextRegion next, ITextRegion previousStart) {
// this (conceptually) abstract method is not concerned with
// specific region types
return false;
}
/**
* Returns the content type of the partition containing the given
* character position of the given document. The document has previously
* been connected to the partitioner.
*
* @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int)
*/
public String getContentType(int offset) {
return getPartition(offset).getType();
}
/**
* To be used by default!
*/
public String getDefaultPartitionType() {
return IStructuredPartitions.DEFAULT_PARTITION;
}
/**
* Returns the set of all possible content types the partitioner supports.
* I.e. Any result delivered by this partitioner may not contain a content
* type which would not be included in this method's result.
*
* @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes()
*/
public java.lang.String[] getLegalContentTypes() {
if (fSupportedTypes == null) {
initLegalContentTypes();
}
return fSupportedTypes;
}
/**
* Returns the partition containing the given character position of the
* given document. The document has previously been connected to the
* partitioner.
*
* Note: this shouldn't be called directly by clients, unless they control
* the threading that includes modifications to the document. Otherwise
* the document could be modified while partitions are being computed. We
* advise that clients use the getPartition API directly from the
* document, so they won't have to worry about that.
*
*
*
* @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int)
*/
public ITypedRegion getPartition(int offset) {
internalGetPartition(offset, true);
return createNewPartitionInstance();
}
protected String getPartitionFromBlockedText(ITextRegion region, int offset, String result) {
// parser sensitive code was moved to subclass for quick transition
// this (conceptually) abstract version isn't concerned with blocked
// text
return result;
}
protected String getPartitionType(ForeignRegion region, int offset) {
String tagname = region.getSurroundingTag();
String result = null;
if (tagname != null) {
result = "BLOCK:" + tagname.toUpperCase(Locale.ENGLISH); //$NON-NLS-1$
} else {
result = "BLOCK"; //$NON-NLS-1$
}
return result;
}
protected String getPartitionType(IBlockedStructuredDocumentRegion blockedStructuredDocumentRegion, int offset) {
String result = null;
ITextRegionList regions = blockedStructuredDocumentRegion.getRegions();
// regions should never be null, or hold zero regions, but just in
// case...
if (regions != null && regions.size() > 0) {
if (regions.size() == 1) {
// if only one, then its a "pure" blocked note.
// if more than one, then must contain some embedded region
// container
ITextRegion blockedRegion = regions.get(0);
// double check for code safefy, though should always be true
if (blockedRegion instanceof ForeignRegion) {
result = getPartitionType((ForeignRegion) blockedRegion, offset);
}
} else {
// must have some embedded region container, so we'll make
// sure we'll get the appropriate one
result = getReleventRegionType(blockedStructuredDocumentRegion, offset);
}
}
return result;
}
/**
* Method getPartitionType.
*
* @param region
* @return String
*/
private String getPartitionType(ITextRegion region) {
// if it get's to this "raw" level, then
// must be default.
return getDefaultPartitionType();
}
/**
* Returns the partition based on region type. This basically maps from
* one region-type space to another, higher level, region-type space.
*
* @param region
* @param offset
* @return String
*/
public String getPartitionType(ITextRegion region, int offset) {
String result = getDefaultPartitionType();
// if (region instanceof ContextRegionContainer) {
// result = getPartitionType((ITextRegionContainer) region, offset);
// } else {
if (region instanceof ITextRegionContainer) {
result = getPartitionType((ITextRegionContainer) region, offset);
}
result = getPartitionFromBlockedText(region, offset, result);
return result;
}
/**
* Similar to method with 'ITextRegion' as argument, except for
* RegionContainers, if it has embedded regions, then we need to drill
* down and return DocumentPartition based on "lowest level" region type.
* For example, in <body id=" <%= object.getID() %>" > The text between
* <%= and %> would be a "java region" not an "HTML region".
*/
protected String getPartitionType(ITextRegionContainer region, int offset) {
// TODO this method needs to be 'cleaned up' after refactoring
// its instanceof logic seems messed up now.
String result = null;
if (region != null) {
ITextRegion coreRegion = region;
if (coreRegion instanceof ITextRegionContainer) {
result = getPartitionType((ITextRegionContainer) coreRegion, ((ITextRegionContainer) coreRegion).getRegions(), offset);
} else {
result = getPartitionType(region);
}
} else {
result = getPartitionType((ITextRegion) region, offset);
}
return result;
}
private String getPartitionType(ITextRegionContainer coreRegion, ITextRegionList regions, int offset) {
String result = null;
for (int i = 0; i < regions.size(); i++) {
ITextRegion region = regions.get(i);
if (coreRegion.containsOffset(region, offset)) {
result = getPartitionType(region, offset);
break;
}
}
return result;
}
/**
* Computes the partition type for the zero-length partition between a
* start tag and end tag with the given name regions.
*
* @param previousStartTagNameRegion
* @param nextEndTagNameRegion
* @return String
*/
public String getPartitionTypeBetween(IStructuredDocumentRegion previousNode, IStructuredDocumentRegion nextNode) {
return getDefaultPartitionType();
}
/**
* Return the ITextRegion at the given offset. For most cases, this will
* be the flatNode itself. Should it contain an embedded
* ITextRegionContainer, will return the internal region at the offset
*
*
* @param flatNode
* @param offset
* @return ITextRegion
*/
private String getReleventRegionType(IStructuredDocumentRegion flatNode, int offset) {
// * Note: the original form of this method -- which returned "deep"
// region, isn't that
// * useful, after doing parent elimination refactoring,
// * since once the deep region is returned, its hard to get its text
// or offset without
// * proper parent.
ITextRegion resultRegion = null;
if (containsEmbeddedRegion(flatNode)) {
resultRegion = flatNode.getRegionAtCharacterOffset(offset);
if (resultRegion instanceof ITextRegionContainer) {
resultRegion = flatNode.getRegionAtCharacterOffset(offset);
ITextRegionList regions = ((ITextRegionContainer) resultRegion).getRegions();
for (int i = 0; i < regions.size(); i++) {
ITextRegion region = regions.get(i);
if (flatNode.getStartOffset(region) <= offset && offset < flatNode.getEndOffset(region)) {
resultRegion = region;
break;
}
}
}
} else {
resultRegion = flatNode;
}
return resultRegion.getType();
}
/**
* To be used, instead of default, when there is some thing surprising
* about are attempt to partition
*/
protected String getUnknown() {
return IStructuredPartitions.UNKNOWN_PARTITION;
}
/**
* to be abstract eventually
*/
protected void initLegalContentTypes() {
fSupportedTypes = new String[]{IStructuredPartitions.DEFAULT_PARTITION, IStructuredPartitions.UNKNOWN_PARTITION};
}
/**
* Returns the partition containing the given character position of the
* given document. The document has previously been connected to the
* partitioner. If the checkBetween parameter is true, an offset between a
* start and end tag will return a zero-length region.
*/
private void internalGetPartition(int offset, boolean checkBetween) {
if (fStructuredDocument == null) {
throw new IllegalStateException("document partitioner is not connected"); //$NON-NLS-1$
}
boolean partitionFound = false;
int docLength = fStructuredDocument.getLength();
// get document region type and map to partition type :
// Note: a partion can be smaller than a flatnode, if that flatnode
// contains a region container.
// That's why we need to get "relevent region".
IStructuredDocumentRegion structuredDocumentRegion = fStructuredDocument.getRegionAtCharacterOffset(offset);
// flatNode is null if empty document
// this is king of a "normal case" for empty document
if (structuredDocumentRegion == null) {
if (docLength == 0) {
/*
* In order to prevent infinite error loops, this partition
* must never have a zero length unless the document is also
* zero length
*/
setInternalPartition(offset, 0, getDefaultPartitionType());
partitionFound = true;
}
else {
/*
* This case is "unusual". When would region be null, and
* document longer than 0. I think this means something's "out
* of sync". And we may want to "flag" that fact and just
* return one big region of 'unknown', instead of one
* character at a time.
*/
setInternalPartition(offset, 1, getUnknown());
partitionFound = true;
}
}
else if (checkBetween) {
// dmw: minimizes out to the first if test above
// if (structuredDocumentRegion == null && docLength == 0) {
// // known special case for an empty document
// setInternalPartition(offset, 0, getDefault());
// partitionFound = true;
// }
// else
if (structuredDocumentRegion.getStartOffset() == offset) {
IStructuredDocumentRegion previousStructuredDocumentRegion = structuredDocumentRegion.getPrevious();
if (previousStructuredDocumentRegion != null) {
ITextRegion next = structuredDocumentRegion.getRegionAtCharacterOffset(offset);
ITextRegion previousStart = previousStructuredDocumentRegion.getRegionAtCharacterOffset(previousStructuredDocumentRegion.getStartOffset());
partitionFound = doParserSpecificCheck(offset, partitionFound, structuredDocumentRegion, previousStructuredDocumentRegion, next, previousStart);
}
}
}
if (!partitionFound && structuredDocumentRegion != null) {
/* We want the actual ITextRegion and not a possible ITextRegionCollection that
* could be returned by IStructuredDocumentRegion#getRegionAtCharacterOffset
* This allows for correct syntax highlighting and content assist.
*/
DeepRegion resultRegion = getDeepRegionAtCharacterOffset(structuredDocumentRegion, offset);
partitionFound = isDocumentRegionBasedPartition(structuredDocumentRegion, resultRegion.region, offset);
if (!partitionFound) {
if (resultRegion.region != null) {
String type = getPartitionType(resultRegion.region, offset);
setInternalPartition(offset, resultRegion.end - offset, type);
} else {
// can happen at EOF
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=224886
// The unknown type was causing problems with content assist in JSP documents
setInternalPartition(offset, 1, getDefaultPartitionType());
}
}
}
}
private static class DeepRegion {
int end;
ITextRegion region;
DeepRegion(ITextRegion r, int e) {
region = r;
end = e;
}
}
/**
* <p>Unlike {@link IStructuredDocumentRegion#getRegionAtCharacterOffset(int)} this will dig
* into <code>ITextRegionCollection</code> to find the region containing the given offset</p>
*
* @param region the containing region of the given <code>offset</code>
* @param offset to the overall offset in the document.
* @return the <code>ITextRegion</code> containing the given <code>offset</code>, will never be
* a <code>ITextRegionCollextion</code>
*/
private DeepRegion getDeepRegionAtCharacterOffset(IStructuredDocumentRegion region, int offset) {
ITextRegion text = region.getRegionAtCharacterOffset(offset);
int end = region.getStartOffset();
if (text != null)
end += text.getStart();
while (text instanceof ITextRegionCollection) {
text = ((ITextRegionCollection) text).getRegionAtCharacterOffset(offset);
end += text.getStart();
}
if (text != null)
end += text.getLength();
return new DeepRegion(text, end);
}
/**
* Provides for a per-StructuredDocumentRegion override selecting the
* partition type using more than just a single ITextRegion.
*
* @param structuredDocumentRegion
* the StructuredDocumentRegion
* @param containedChildRegion
* an ITextRegion within the given StructuredDocumentRegion
* that would normally determine the partition type by itself
* @param offset
* the document offset
* @return true if the partition type will be overridden, false to
* continue normal processing
*/
protected boolean isDocumentRegionBasedPartition(IStructuredDocumentRegion structuredDocumentRegion, ITextRegion containedChildRegion, int offset) {
return false;
}
public IDocumentPartitioner newInstance() {
return new StructuredTextPartitioner();
}
protected void setInternalPartition(int offset, int length, String type) {
synchronized (PARTITION_LOCK) {
internalReusedTempInstance.setOffset(offset);
internalReusedTempInstance.setLength(length);
internalReusedTempInstance.setType(type);
}
}
}