/* * Copyright (c) 2002-2004 Widespace, OU and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://solareclipse.sourceforge.net/legal/cpl-v10.html * * Contributors: * Igor Malinin - initial contribution * * $Id: AbstractPartitioner.java,v 1.3 2004-12-29 21:42:31 axelcl Exp $ */ package net.sourceforge.phpeclipse.ui.text.rules; import java.util.ArrayList; import java.util.List; import org.eclipse.jface.text.Assert; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentPartitioner; import org.eclipse.jface.text.IDocumentPartitionerExtension; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.text.TypedRegion; import org.eclipse.jface.text.rules.IPartitionTokenScanner; import org.eclipse.jface.text.rules.IToken; /** * Advanced partitioner which maintains partitions as views to connected document. Views have own partitioners themselves. This * class is designed as a base for complex partitioners such as for JSP, PHP, ASP, etc. languages. * * @author Igor Malinin */ public abstract class AbstractPartitioner implements IDocumentPartitioner, IDocumentPartitionerExtension { public final static boolean DEBUG = false; /** Partition scanner */ protected IPartitionTokenScanner scanner; /** Connected document */ protected IDocument document; /** Flat structure of the document */ protected List nodes = new ArrayList(); /** The offset at which the first changed partition starts */ protected int regionStart; /** The offset at which the last changed partition ends */ protected int regionEnd; public AbstractPartitioner(IPartitionTokenScanner scanner) { this.scanner = scanner; } protected FlatNode createNode(String type, int offset, int length) { if (DEBUG) { Assert.isTrue(offset >= 0, Integer.toString(offset)); } FlatNode node = new FlatNode(type); node.offset = offset; node.length = length; return node; } protected void addInnerRegion(FlatNode position) { nodes.add(computeFlatNodeIndex(position.offset), position); } protected void removeInnerRegion(FlatNode position) { nodes.remove(position); // TODO: Indexed remove? } protected void deleteInnerRegion(FlatNode position) { nodes.remove(position); // TODO: Indexed remove? } protected void resizeInnerRegion(FlatNode position) { } /* * @see org.eclipse.jface.text.IDocumentPartitioner#connect(IDocument) */ public void connect(IDocument document) { this.document = document; initialize(); } /* * @see org.eclipse.jface.text.IDocumentPartitioner#disconnect() */ public void disconnect() { nodes.clear(); document = null; } /** * Performs the initial partitioning of the partitioner's document. */ protected void initialize() { scanner.setRange(document, 0, document.getLength()); // axelcl start nodes.clear(); // axelcl end IToken token = scanner.nextToken(); while (!token.isEOF()) { String contentType = getTokenContentType(token); if (isSupportedContentType(contentType)) { addInnerRegion(createNode(contentType, scanner.getTokenOffset(), scanner.getTokenLength())); } token = scanner.nextToken(); } } /* * @see org.eclipse.jface.text.IDocumentPartitioner#documentAboutToBeChanged(DocumentEvent) */ public void documentAboutToBeChanged(DocumentEvent event) { regionStart = regionEnd = -1; } /* * @see org.eclipse.jface.text.IDocumentPartitioner#documentChanged(DocumentEvent) */ public boolean documentChanged(DocumentEvent event) { return (documentChanged2(event) != null); } /* * @see org.eclipse.jface.text.IDocumentPartitionerExtension#documentChanged2(DocumentEvent) */ public IRegion documentChanged2(DocumentEvent event) { int first = fixupPartitions(event); FlatNode[] category = (FlatNode[]) nodes.toArray(new FlatNode[nodes.size()]); // repartition changed region String contentType = IDocument.DEFAULT_CONTENT_TYPE; int offset; if (first == 0) { offset = 0; // Bug #697414: first offset } else { offset = event.getOffset(); FlatNode partition = category[first - 1]; if (partition.includes(offset)) { offset = partition.offset; contentType = partition.type; --first; } else if (offset == partition.offset + partition.length) { offset = partition.offset; contentType = partition.type; --first; } else { offset = partition.offset + partition.length; } } // should not be changed since last conversion // category = (FlatNode[]) nodes.toArray(new FlatNode[nodes.size()]); if (DEBUG) { Assert.isTrue(offset >= 0, Integer.toString(offset)); } scanner.setPartialRange(document, offset, document.getLength(), contentType, offset); int lastScannedPosition = offset; IToken token = scanner.nextToken(); while (!token.isEOF()) { contentType = getTokenContentType(token); if (!isSupportedContentType(contentType)) { token = scanner.nextToken(); continue; } offset = scanner.getTokenOffset(); if (DEBUG) { Assert.isTrue(offset >= 0, scanner.toString()); } int length = scanner.getTokenLength(); lastScannedPosition = offset + length; // remove all affected positions while (first < category.length) { FlatNode p = category[first]; if (p.offset + p.length < lastScannedPosition || (p.overlapsWith(offset, length) && (!containsPosition(offset, length) || !contentType.equals(p.type)))) { removeInnerRegion(p); rememberRegion(p.offset, p.length); ++first; } else { break; } } // if position already exists we are done if (containsPosition(offset, length)) { if (lastScannedPosition > event.getOffset()) { // TODO: optional repartition till end of doc return createRegion(); } ++first; } else { // insert the new type position addInnerRegion(createNode(contentType, offset, length)); rememberRegion(offset, length); } // try { token = scanner.nextToken(); // } catch (ArrayIndexOutOfBoundsException e) { // System.out.println(this.getClass().toString()); // throw e; // } } // remove all positions behind lastScannedPosition // since there aren't any further types // Do not need to recalculate (lost remove events)! // first = computeIndexInInnerDocuments(lastScannedPosition); while (first < category.length) { FlatNode p = category[first++]; removeInnerRegion(p); rememberRegion(p.offset, p.length); } return createRegion(); } protected int fixupPartitions(DocumentEvent event) { int offset = event.getOffset(); int length = event.getLength(); int end = offset + length; // fixup flat nodes laying on change boundaries int first = computeFlatNodeIndex(offset); if (first > 0) { FlatNode p = (FlatNode) nodes.get(first - 1); int right = p.offset + p.length; if (offset < right) { // change overlaps with partition if (end < right) { // cahnge completely inside partition String text = event.getText(); p.length -= length; if (text != null) { p.length += text.length(); } } else { // cut partition at right int cut = p.offset + p.length - offset; p.length -= cut; } } } int last = computeFlatNodeIndex(end); if (first < last) { FlatNode p = (FlatNode) nodes.get(last - 1); int right = p.offset + p.length; if (end < right) { // cut partition at left int cut = end - p.offset; p.length -= cut; p.offset = offset; String text = event.getText(); if (text != null) { p.offset += text.length(); } --last; } } // fixup flat nodes laying afrer change String text = event.getText(); if (text != null) { length -= text.length(); } for (int i = last, size = nodes.size(); i < size; i++) { ((FlatNode) nodes.get(i)).offset -= length; } // delete flat nodes laying completely inside change boundaries if (first < last) { do { deleteInnerRegion((FlatNode) nodes.get(--last)); } while (first < last); rememberRegion(offset, 0); } return first; } /** * Returns whether the given type is one of the legal content types. * * @param contentType * the content type to check * @return true if the content type is a legal content type */ protected boolean isSupportedContentType(String contentType) { /* TODO: implementation */ // if (contentType != null) { // for (int i= 0; i < fLegalContentTypes.length; i++) { // if (fLegalContentTypes[i].equals(contentType)) { // return true; // } // } // } // return false; return (contentType != null); } /** * Returns a content type encoded in the given token. If the token's data is not null and a string it is assumed * that it is the encoded content type. * * @param token * the token whose content type is to be determined * @return the token's content type */ protected String getTokenContentType(IToken token) { Object data = token.getData(); if (data instanceof String) { return (String) data; } return null; } /* * @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes() */ public String[] getLegalContentTypes() { // TODO: implementation return null; } /* * @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int) */ public String getContentType(int offset) { return getPartition(offset).getType(); } /* * @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int) */ public ITypedRegion getPartition(int offset) { if (nodes.size() == 0) { return new TypedRegion(0, document.getLength(), IDocument.DEFAULT_CONTENT_TYPE); } int index = computeFlatNodeIndex(offset); if (index < nodes.size()) { FlatNode next = (FlatNode) nodes.get(index); if (offset == next.offset) { return new TypedRegion(next.offset, next.length, next.type); } if (index == 0) { return new TypedRegion(0, next.offset, IDocument.DEFAULT_CONTENT_TYPE); } FlatNode prev = (FlatNode) nodes.get(index - 1); if (prev.includes(offset)) { return new TypedRegion(prev.offset, prev.length, prev.type); } int end = prev.offset + prev.length; return new TypedRegion(end, next.offset - end, IDocument.DEFAULT_CONTENT_TYPE); } FlatNode prev = (FlatNode) nodes.get(nodes.size() - 1); if (prev.includes(offset)) { return new TypedRegion(prev.offset, prev.length, prev.type); } int end = prev.offset + prev.length; return new TypedRegion(end, document.getLength() - end, IDocument.DEFAULT_CONTENT_TYPE); } /* * @see org.eclipse.jface.text.IDocumentPartitioner#computePartitioning(int, int) */ public ITypedRegion[] computePartitioning(int offset, int length) { List list = new ArrayList(); int end = offset + length; int index = computeFlatNodeIndex(offset); while (true) { FlatNode prev = (index > 0) ? (FlatNode) nodes.get(index - 1) : null; if (prev != null) { if (prev.overlapsWith(offset, length)) { list.add(new TypedRegion(prev.offset, prev.length, prev.type)); } if (end <= prev.offset + prev.length) { break; } } FlatNode next = (index < nodes.size()) ? (FlatNode) nodes.get(index) : null; if (next == null || offset < next.offset) { int off0 = offset; int off1 = offset + length; if (prev != null && off0 < prev.offset + prev.length) { off0 = prev.offset + prev.length; } if (next != null && next.offset < off1) { off1 = next.offset; } if (off0 < off1) { list.add(new TypedRegion(off0, off1 - off0, IDocument.DEFAULT_CONTENT_TYPE)); } } if (next == null) { break; } ++index; } return (TypedRegion[]) list.toArray(new TypedRegion[list.size()]); } /** * Computes the index in the list of flat nodes at which an flat node with the given offset would be inserted. The flat node is * supposed to become the first in this list of all flat nodes with the same offset. * * @param offset * the offset for which the index is computed * @return the computed index */ protected int computeFlatNodeIndex(int offset) { if (nodes.size() == 0) { return 0; } int left = 0, mid = 0; int right = nodes.size() - 1; FlatNode p = null; while (left < right) { mid = (left + right) / 2; p = (FlatNode) nodes.get(mid); if (offset < p.offset) { right = (left == mid) ? left : mid - 1; } else if (offset > p.offset) { left = (right == mid) ? right : mid + 1; } else if (offset == p.offset) { left = right = mid; } } int pos = left; p = (FlatNode) nodes.get(pos); if (offset > p.offset) { // append to the end pos++; } else { // entry will became the first of all entries with the same offset do { --pos; if (pos < 0) { break; } p = (FlatNode) nodes.get(pos); } while (offset == p.offset); ++pos; } return pos; } public boolean containsPosition(int offset, int length) { int size = nodes.size(); if (size == 0) { return false; } int index = computeFlatNodeIndex(offset); if (index < size) { FlatNode p = (FlatNode) nodes.get(index); while (p.offset == offset) { if (p.length == length) { return true; } if (++index < size) { p = (FlatNode) nodes.get(index); } else { break; } } } return false; } /** * Helper method for tracking the minimal region containg all partition changes. If offset is smaller than the * remembered offset, offset will from now on be remembered. If offset + length is greater than the * remembered end offset, it will be remembered from now on. * * @param offset * the offset * @param length * the length */ protected final void rememberRegion(int offset, int length) { // remember start offset if (regionStart == -1) { regionStart = offset; } else if (offset < regionStart) { regionStart = offset; } // remember end offset int endOffset = offset + length; if (regionEnd == -1) { regionEnd = endOffset; } else if (endOffset > regionEnd) { regionEnd = endOffset; } } /** * Creates the minimal region containing all partition changes using the remembered offsets. * * @return the minimal region containing all the partition changes */ protected final IRegion createRegion() { if (regionStart == -1 || regionEnd == -1) { return null; } return new Region(regionStart, regionEnd - regionStart); } }