| /******************************************************************************* |
| * Copyright (c) 2001, 2011 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; |
| } |
| |
| protected IStructuredDocumentRegion getParserSpecificPreviousRegion(IStructuredDocumentRegion currentRegion) { |
| return currentRegion != null ? currentRegion.getPrevious() : null; |
| } |
| |
| /** |
| * 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 = getParserSpecificPreviousRegion(structuredDocumentRegion); |
| 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); |
| } |
| } |
| |
| } |