blob: a569073381989ceb7e38f84b50d1c3f6033a04eb [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2014, 2021 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ecommons.text.core.treepartitioner;
import java.util.List;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.BadPositionCategoryException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
/**
* Scan implementation for {@link TreePartitioner}.
*/
@NonNullByDefault
final class TreePartitionerScan implements TreePartitionNodeScan {
private static final int END_FLAGS= TreePartitionNode.END_UNCLOSED;
private final TreePartitioner partitioner;
private int startOffset;
private int endOffset;
private NodePosition beginPosition;
private NodePosition lastParentPosition;
private int lastAddedIndex;
private @Nullable NodePosition lastAddedPosition;
private int currentOffset;
private int dirtyStartOffset;
private int dirtyEndOffset;
private int stamp;
private int equalTypeEndOffset;
private boolean autoBreakEnabled;
public TreePartitionerScan(final TreePartitioner partitioner) {
this.partitioner= partitioner;
}
void init(final int startOffset, final int endOffset, final NodePosition beginPosition) {
this.startOffset= startOffset;
this.endOffset= endOffset;
this.beginPosition= beginPosition;
this.lastParentPosition= beginPosition;
{ int childIdx= beginPosition.indexOfChild(startOffset);
if (childIdx < 0) {
childIdx= -(childIdx + 1);
}
this.lastAddedIndex= childIdx - 1;
}
this.currentOffset= startOffset;
this.dirtyStartOffset= Integer.MAX_VALUE;
this.dirtyEndOffset= -1;
this.stamp++;
this.equalTypeEndOffset= Integer.MIN_VALUE;
this.autoBreakEnabled= true;
}
void addBeginPosition(final TreePartitionNodeType type) {
this.beginPosition= doAdd(type, this.beginPosition, this.startOffset, 0);
}
@Override
public IDocument getDocument() {
return this.partitioner.document;
}
@Override
public int getStartOffset() {
return this.startOffset;
}
@Override
public int getEndOffset() {
return this.endOffset;
}
@Override
public TreePartitionNode getBeginNode() {
return this.beginPosition;
}
@Override
public void setAutoBreakEnabled(final boolean enable) {
this.autoBreakEnabled= enable;
}
@Override
public boolean isAutoBreakEnabled() {
return this.autoBreakEnabled;
}
@Override
public NodePosition add(final TreePartitionNodeType type,
final TreePartitionNode parent, final int offset,
final int flags) {
final NodePosition position= doAdd(type, (NodePosition) parent, offset, 0);
position.flags= ((position.flags & END_FLAGS) | flags);
if (TreePartitioner.DEBUG) {
this.partitioner.check(false);
}
if (this.autoBreakEnabled) {
checkBreak();
}
return position;
}
private NodePosition doAdd(final TreePartitionNodeType type,
final NodePosition parentPosition, final int offset, final int length) {
if (type == null) {
throw new NullPointerException("type"); //$NON-NLS-1$
}
if (parentPosition == null) {
throw new NullPointerException("parent"); //$NON-NLS-1$
}
if (offset < parentPosition.getOffset()) {
throw new IllegalArgumentException("offset: offset < parent.offset"); //$NON-NLS-1$
}
if (length < 0) {
throw new IllegalArgumentException("length: length < 0"); //$NON-NLS-1$
}
NodePosition position= null;
final int endOffset= offset + length;
int childIdx;
if (offset >= this.currentOffset) {
childIdx= cleanUpTo(parentPosition);
position= findReuse(parentPosition.children, childIdx, type, offset);
if (position != null && length <= position.getLength()) {
int end= endOffset;
if (!position.children.isEmpty()) {
final int firstOffset= position.children.get(0).getOffset();
if (firstOffset < end) {
end= firstOffset;
}
}
this.equalTypeEndOffset= end;
}
doDeleteChildren(parentPosition, childIdx, endOffset, position);
if (parentPosition.getEndOffset() < endOffset) {
doUpdateEnd(parentPosition, endOffset);
}
if (position == null) {
position= createPosition(parentPosition, offset, length, childIdx, type);
}
this.lastParentPosition= parentPosition;
this.lastAddedIndex= childIdx;
this.lastAddedPosition= position;
this.currentOffset= offset;
}
else {
if (offset < parentPosition.offset || endOffset >= parentPosition.getEndOffset()) {
throw new IllegalArgumentException("node not inside parent");
}
childIdx= parentPosition.indexOfChild(offset);
if (childIdx < 0) {
childIdx= -(childIdx + 1);
}
while (childIdx < parentPosition.children.size()) {
final NodePosition child= parentPosition.children.get(childIdx);
if (child.getOffset() == offset && child.getLength() == 0) {
childIdx++;
continue;
}
else if (child.getOffset() > offset) {
break;
}
throw new IllegalArgumentException("node overlaps with existing node");
}
position= createPosition(parentPosition, offset, length, childIdx, type);
this.lastParentPosition= parentPosition;
this.lastAddedIndex= childIdx;
this.lastAddedPosition= position;
this.currentOffset= offset;
}
return position;
}
private int cleanUpTo(final NodePosition parentPosition) {
int childIdx;
if (parentPosition == this.lastParentPosition) {
childIdx= this.lastAddedIndex + 1;
}
else if (parentPosition == this.lastAddedPosition) {
childIdx= 0;
}
else {
// step down
childIdx= this.lastAddedIndex + 1;
NodePosition p= this.lastParentPosition;
do {
doDeleteChildren(p, childIdx, Integer.MAX_VALUE, null);
childIdx= p.parent.indexOfChild(p) + 1;
p= p.parent;
}
while (parentPosition != p);
}
return childIdx;
}
private NodePosition createPosition(final NodePosition parentPosition,
final int offset, final int length, final int childIdx, final TreePartitionNodeType type) {
markDirtyRegion(offset, length);
final NodePosition position= new NodePosition.CommonNode(parentPosition, offset, length,
type, this.stamp );
parentPosition.insertChild(childIdx, position);
try {
this.partitioner.addPosition(position);
}
catch (final BadPositionCategoryException e) {}
catch (final BadLocationException e) {
throw new RuntimeException(e);
}
return position;
}
private NodePosition findReuse(final List<NodePosition> children, int childIdx,
final TreePartitionNodeType type, final int offset) {
for (; childIdx < children.size(); childIdx++) {
final NodePosition child= children.get(childIdx);
if (child.getOffset() == offset && type.equals(child.type)) {
child.type= type;
return child;
}
if (child.getOffset() > offset) {
break;
}
}
return null;
}
@Override
public void expand(final TreePartitionNode node, final int offset, final int flags, final boolean close) {
final NodePosition position= (NodePosition) node;
if (position == null) {
throw new IllegalArgumentException("node"); //$NON-NLS-1$
}
final int length= offset - position.getOffset();
if (length < 0) {
throw new IllegalArgumentException("offset: offset < node.offset"); //$NON-NLS-1$
}
if (offset >= this.currentOffset) {
int childIdx;
childIdx= cleanUpTo(position);
doDeleteChildren(position, childIdx, (close) ? Integer.MAX_VALUE : offset, null);
this.lastParentPosition= position;
this.lastAddedIndex= childIdx - 1;
this.lastAddedPosition= (childIdx > 0) ? position.children.get(childIdx - 1) : null;
this.currentOffset= offset;
}
else {
final List<NodePosition> children= position.children;
if (!children.isEmpty() && children.get(children.size() - 1).getEndOffset() > offset) {
throw new IllegalArgumentException("offset: offset < endOffset of children");
}
}
if ((close) ? (position.getLength() == length) : (position.getLength() >= length)) {
if (position.stamp != this.stamp && offset > this.equalTypeEndOffset) {
this.equalTypeEndOffset= offset;
}
}
else {
doUpdateEnd(position, offset);
}
position.flags= (close) ?
((position.flags & ~END_FLAGS) | flags) :
(position.flags | flags);
if (TreePartitioner.DEBUG) {
this.partitioner.check(false);
}
if (this.autoBreakEnabled) {
checkBreak();
}
}
/** Sets the end/length and updates all parents */
private void doUpdateEnd(NodePosition position, final int endOffset) {
markDirtyEnd(Math.max(endOffset, position.getEndOffset()));
position.setLength(endOffset - position.getOffset());
position.stamp= this.stamp;
// update parents
while (position.parent != null) {
final int parentLength= position.parent.getLength();
final int parentOffset= position.parent.getOffset();
// remove other children in the changed range
doDeleteChildren(position.parent, position.parent.indexOfChild(position),
endOffset, position );
if (parentOffset + parentLength >= endOffset) {
return;
}
else { // expand and continue with parent
position.parent.setLength(endOffset - parentOffset);
position.parent.stamp= this.stamp;
}
position= position.parent;
}
}
private void doDeleteChildren(final NodePosition parentPosition,
int childIdx, final int endOffset, final NodePosition keepPosition) {
final List<NodePosition> children= parentPosition.children;
for (; childIdx < children.size(); ) {
final NodePosition child= children.get(childIdx);
if (child == keepPosition) {
childIdx++;
continue;
}
if (child.getOffset() < endOffset
|| (child.getOffset() == endOffset && child.getLength() == 0) ) {
markDirtyRegion(child.getOffset(), child.getLength());
children.remove(childIdx);
try {
doDelete(child);
}
catch (final BadPositionCategoryException e) {}
continue;
}
break;
}
}
private void doDelete(final NodePosition position) throws BadPositionCategoryException {
if (position.parent != null) {
position.parent= null;
position.isDeleted= true;
this.partitioner.removePosition(position);
final List<NodePosition> children= position.children;
for (int childIdx= 0; childIdx < children.size(); childIdx++) {
doDelete(children.get(childIdx));
}
}
}
void markDirtyRegion(final int offset, final int length) {
markDirtyStart(offset);
markDirtyEnd(offset + length);
}
public void markDirtyStart(final int offset) {
if (offset < this.dirtyStartOffset) {
this.dirtyStartOffset= offset;
}
}
@Override
public void markDirtyEnd(final int offset) {
if (offset > this.dirtyEndOffset) {
this.dirtyEndOffset= offset;
}
}
/**
* Creates the minimal region containing all partition changes using the
* remembered offset, end offset, and deletion offset.
*
* @return the minimal region containing all the partition changes
*/
@Nullable IRegion createDirtyRegion() {
if (this.dirtyEndOffset < 0 || this.dirtyStartOffset == Integer.MAX_VALUE) {
return null;
}
return new Region(this.dirtyStartOffset,
Math.min(getDocument().getLength(), this.dirtyEndOffset) - this.dirtyStartOffset );
}
public boolean canBreak() {
return (this.equalTypeEndOffset >= this.dirtyEndOffset);
}
@Override
public void checkBreak() {
if (canBreak()) {
throw new BreakException();
}
}
}