/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* 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:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.api.editor.partition;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import org.eclipse.che.ide.api.editor.events.DocumentChangeEvent;
import org.eclipse.che.ide.api.editor.text.BadLocationException;
import org.eclipse.che.ide.api.editor.text.BadPositionCategoryException;
import org.eclipse.che.ide.api.editor.text.Position;
import org.eclipse.che.ide.api.editor.text.TypedPosition;
import org.eclipse.che.ide.api.editor.text.TypedRegion;
import org.eclipse.che.ide.api.editor.text.TypedRegionImpl;
import org.eclipse.che.ide.api.editor.text.rules.Token;
import org.eclipse.che.ide.api.editor.document.DocumentHandle;
import org.eclipse.che.ide.util.loging.Log;
/**
* Default implementation of the {@link DocumentPartitioner}.
*/
public class DefaultPartitioner implements DocumentPartitioner {
/** The identifier of the default partitioning. */
public final static String DEFAULT_PARTITIONING = "__dftl_partitioning";
private final PartitionScanner scanner;
private final DocumentPositionMap documentPositionMap;
private DocumentHandle documentHandle;
/** The legal content types of this partitioner */
private final List<String> legalContentTypes;
/** The position category this partitioner uses to store the document's partitioning information. */
private final String positionCategory;
public DefaultPartitioner(final PartitionScanner scanner,
final List<String> legalContentTypes,
final DocumentPositionMap documentPositionMap) {
this.legalContentTypes = new ArrayList<>(legalContentTypes);
this.scanner = scanner;
this.documentPositionMap = documentPositionMap;
this.positionCategory = DocumentPositionMap.Categories.DEFAULT_CATEGORY;
}
@Override
public void initialize() {
this.documentPositionMap.addPositionCategory(this.positionCategory);
this.documentPositionMap.setContentLength(this.documentHandle.getDocument().getContentsCharCount());
}
private void updatePositions() {
// set before the scan as the scan uses the content length
this.documentPositionMap.setContentLength(getContentLength());
this.documentPositionMap.resetPositions();
Position current = null;
try {
Token token = scanner.nextToken();
while (!token.isEOF()) {
final String contentType = getTokenContentType(token);
if (isSupportedContentType(contentType)) {
final TypedPosition position = new TypedPosition(scanner.getTokenOffset(), scanner.getTokenLength(), contentType);
current = position;
this.documentPositionMap.addPosition(this.positionCategory, position);
}
token = scanner.nextToken();
}
} catch (final BadLocationException x) {
Log.error(DefaultPartitioner.class, "Invalid position: " + String.valueOf(current) + " (max:" + getContentLength() + ").", x);
} catch (final BadPositionCategoryException x) {
Log.error(DefaultPartitioner.class, "Invalid position category: " + this.positionCategory, x);
}
}
@Override
public void onDocumentChange(final DocumentChangeEvent event) {
this.scanner.setScannedString(event.getDocument().getDocument().getContents());
updatePositions();
}
@Override
public List<String> getLegalContentTypes() {
return new ArrayList<>(this.legalContentTypes);
}
@Override
public String getContentType(final int offset) {
final TypedPosition position = findClosestPosition(offset);
if (position != null && position.includes(offset)) {
return position.getType();
}
return DEFAULT_CONTENT_TYPE;
}
@Override
public final List<TypedRegion> computePartitioning(final int offset, final int length) {
return computePartitioning(offset, length, false);
}
private List<TypedRegion> computePartitioning(final int offset,
final int length,
final boolean includeZeroLengthPartitions) {
final List<TypedRegion> result = new ArrayList<>();
final int contentLength = getContentLength();
try {
final int endOffset = offset + length;
final List<TypedPosition> category = getPositions();
TypedPosition previous = null;
TypedPosition current = null;
int start, end, gapOffset;
final Position gap = new Position(0);
final int startIndex = getFirstIndexEndingAfterOffset(category, offset);
final int endIndex = getFirstIndexStartingAfterOffset(category, endOffset);
for (int i = startIndex; i < endIndex; i++) {
current = category.get(i);
gapOffset = (previous != null) ? previous.getOffset() + previous.getLength() : 0;
gap.setOffset(gapOffset);
gap.setLength(current.getOffset() - gapOffset);
if ((includeZeroLengthPartitions && overlapsOrTouches(gap, offset, length))
|| (gap.getLength() > 0 && gap.overlapsWith(offset, length))) {
start = Math.max(offset, gapOffset);
end = Math.min(endOffset, gap.getOffset() + gap.getLength());
result.add(new TypedRegionImpl(start, end - start, DEFAULT_CONTENT_TYPE));
}
if (current.overlapsWith(offset, length)) {
start = Math.max(offset, current.getOffset());
end = Math.min(endOffset, current.getOffset() + current.getLength());
result.add(new TypedRegionImpl(start, end - start, current.getType()));
}
previous = current;
}
if (previous != null) {
gapOffset = previous.getOffset() + previous.getLength();
gap.setOffset(gapOffset);
gap.setLength(contentLength - gapOffset);
if ((includeZeroLengthPartitions && overlapsOrTouches(gap, offset, length)) ||
(gap.getLength() > 0 && gap.overlapsWith(offset, length))) {
start = Math.max(offset, gapOffset);
end = Math.min(endOffset, contentLength);
result.add(new TypedRegionImpl(start, end - start, DEFAULT_CONTENT_TYPE));
}
}
if (result.isEmpty()) {
result.add(new TypedRegionImpl(offset, length, DEFAULT_CONTENT_TYPE));
}
} catch (final BadPositionCategoryException ex) {
Logger.getLogger(DefaultPartitioner.class.getName()).fine("Bad position in computePartitioning.");
} catch (final RuntimeException ex) {
Logger.getLogger(DefaultPartitioner.class.getName()).warning("computePartitioning failed.");
throw ex;
}
return result;
}
@Override
public TypedRegion getPartition(final int offset) {
final int contentLength = getContentLength();
List<TypedPosition> category = null;
try {
category = getPositions();
} catch (final BadPositionCategoryException e) {
Log.warn(DefaultPartitioner.class, "Invalid position cateory... with default category! ", e);
return defaultRegion();
}
if (category == null || category.size() == 0) {
return defaultRegion();
}
Integer index = null;
try {
index = this.documentPositionMap.computeIndexInCategory(positionCategory, offset);
} catch (final BadLocationException e) {
Log.warn(DefaultPartitioner.class, "Invalid location " + offset + " (max=" + contentLength + ").");
return defaultRegion();
} catch (final BadPositionCategoryException e) {
Log.warn(DefaultPartitioner.class, "Invalid position cateory... with default category " + positionCategory + "!", e);
return defaultRegion();
}
if (index == null) {
return defaultRegion();
}
if (index < category.size()) {
final TypedPosition next = category.get(index);
if (offset == next.offset) {
return new TypedRegionImpl(next.getOffset(),
next.getLength(),
next.getType());
}
if (index == 0) {
return new TypedRegionImpl(0, next.offset, DEFAULT_CONTENT_TYPE);
}
final TypedPosition previous = category.get(index - 1);
if (previous.includes(offset)) {
return new TypedRegionImpl(previous.getOffset(),
previous.getLength(),
previous.getType());
}
final int endOffset = previous.getOffset() + previous.getLength();
return new TypedRegionImpl(endOffset,
next.getOffset() - endOffset,
DEFAULT_CONTENT_TYPE);
}
final TypedPosition previous = category.get(category.size() - 1);
if (previous.includes(offset)) {
return new TypedRegionImpl(previous.getOffset(),
previous.getLength(),
previous.getType());
}
final int endOffset = previous.getOffset() + previous.getLength();
return new TypedRegionImpl(endOffset,
contentLength - endOffset,
DEFAULT_CONTENT_TYPE);
}
private TypedRegionImpl defaultRegion() {
return new TypedRegionImpl(0, getContentLength(), DEFAULT_CONTENT_TYPE);
}
/**
* Returns <code>true</code> if the given ranges overlap with or touch each other.
*
* @param gap the first range
* @param offset the offset of the second range
* @param length the length of the second range
* @return <code>true</code> if the given ranges overlap with or touch each other
*/
private static boolean overlapsOrTouches(Position gap, int offset, int length) {
return gap.getOffset() <= offset + length && offset <= gap.getOffset() + gap.getLength();
}
/**
* Returns the index of the first position which ends after the given offset.
*
* @param positions the positions in linear order
* @param offset the offset
* @return the index of the first position which ends after the offset
*/
private static int getFirstIndexEndingAfterOffset(final List<TypedPosition> positions, final int offset) {
int i = -1;
int j = positions.size();
while (j - i > 1) {
final int k = (i + j) >> 1;
final Position p = positions.get(k);
if (p.getOffset() + p.getLength() > offset) {
j = k;
} else {
i = k;
}
}
return j;
}
/**
* Returns the index of the first position which starts at or after the given offset.
*
* @param positions the positions in linear order
* @param offset the offset
* @return the index of the first position which starts after the offset
*/
private static int getFirstIndexStartingAfterOffset(List<TypedPosition> positions, int offset) {
int i = -1;
int j = positions.size();
while (j - i > 1) {
final int k = (i + j) >> 1;
final Position p = positions.get(k);
if (p.getOffset() >= offset) {
j = k;
} else {
i = k;
}
}
return j;
}
/**
* Returns a content type encoded in the given token. If the token's data is not <code>null</code> and a string it is assumed that it is
* the encoded content type.
* <p>
* May be replaced or extended by subclasses.
* </p>
*
* @param token the token whose content type is to be determined
* @return the token's content type
*/
protected static String getTokenContentType(Token token) {
final Object data = token.getData();
if (data instanceof String) {
return (String)data;
}
return null;
}
/**
* Returns whether the given type is one of the legal content types.
* <p>
* May be extended by subclasses.
* </p>
*
* @param contentType the content type to check
* @return <code>true</code> if the content type is a legal content type
*/
protected boolean isSupportedContentType(final String contentType) {
if (contentType != null) {
for (final String item : this.legalContentTypes) {
if (item.equals(contentType)) {
return true;
}
}
}
return false;
}
/**
* Returns the position in the partitoner's position category which is close to the given offset. This is, the position has either an
* offset which is the same as the given offset or an offset which is smaller than the given offset. This method profits from the
* knowledge that a partitioning is a ordered set of disjoint position.
* <p>
* May be extended or replaced by subclasses.
* </p>
*
* @param offset the offset for which to search the closest position
* @return the closest position in the partitioner's category
*/
protected TypedPosition findClosestPosition(int offset) {
int index = -1;
try {
index = this.documentPositionMap.computeIndexInCategory(this.positionCategory, offset);
} catch (final BadLocationException e) {
Log.warn(DefaultPartitioner.class, "Bad location: " + offset + "(max:" + getContentLength() + ").");
return null;
} catch (final BadPositionCategoryException e) {
Log.warn(DefaultPartitioner.class, "Bad position category: " + this.positionCategory);
return null;
}
if (index == -1) {
return null;
}
List<TypedPosition> category = null;
try {
category = getPositions();
} catch (final BadPositionCategoryException e) {
Log.warn(DefaultPartitioner.class, "Bad position category: " + this.positionCategory);
return null;
}
if (category == null || category.size() == 0) {
return null;
}
if (index < category.size()) {
if (offset == category.get(index).offset) {
return category.get(index);
}
}
if (index > 0) {
index--;
}
return category.get(index);
}
/**
* Returns the partitioners positions.
*
* @return the partitioners positions
* @throws BadPositionCategoryException if getting the positions from the document fails
*/
protected final List<TypedPosition> getPositions() throws BadPositionCategoryException {
return this.documentPositionMap.getPositions(this.positionCategory);
}
private int getContentLength() {
return getDocumentHandle().getDocument().getContentsCharCount();
}
@Override
public DocumentHandle getDocumentHandle() {
return documentHandle;
}
@Override
public void setDocumentHandle(DocumentHandle handle) {
this.documentHandle = handle;
}
@Override
public void release() {
}
}