/**
*
*/
package com.sap.furcas.runtime.textblocks.model;
import static com.sap.furcas.runtime.textblocks.TbNavigationUtil.getLevel;
import static com.sap.furcas.runtime.textblocks.TbNavigationUtil.getSubNodeAt;
import static com.sap.furcas.runtime.textblocks.TbNavigationUtil.getSubNodes;
import static com.sap.furcas.runtime.textblocks.TbNavigationUtil.isFirstInSubTree;
import static com.sap.furcas.runtime.textblocks.TbUtil.getAbsoluteOffset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.compare.contentmergeviewer.TokenComparator;
import org.eclipse.compare.rangedifferencer.RangeDifference;
import org.eclipse.compare.rangedifferencer.RangeDifferencer;
import com.sap.furcas.metamodel.FURCAS.textblocks.AbstractToken;
import com.sap.furcas.metamodel.FURCAS.textblocks.Bostoken;
import com.sap.furcas.metamodel.FURCAS.textblocks.DocumentNode;
import com.sap.furcas.metamodel.FURCAS.textblocks.Eostoken;
import com.sap.furcas.metamodel.FURCAS.textblocks.LexedToken;
import com.sap.furcas.metamodel.FURCAS.textblocks.TextBlock;
import com.sap.furcas.metamodel.FURCAS.textblocks.Version;
import com.sap.furcas.runtime.textblocks.CoverageBean;
import com.sap.furcas.runtime.textblocks.TbNavigationUtil;
import com.sap.furcas.runtime.textblocks.TbUtil;
import com.sap.furcas.runtime.textblocks.modifcation.TbChangeUtil;
import com.sap.furcas.runtime.textblocks.modifcation.TbReplacingHelper;
import com.sap.furcas.runtime.textblocks.modifcation.TbVersionUtil;
import com.sap.furcas.runtime.textblocks.shortprettyprint.ShortPrettyPrinter;
/**
* A model of a textBlocks tree.
*/
public class TextBlocksModel {
public static class TokenChange {
public final LexedToken token;
public final int oldOffset;
public final int oldLength;
public TokenChange(LexedToken token, int oldOffset, int oldLength) {
this.token = token;
this.oldOffset = oldOffset;
this.oldLength = oldLength;
}
}
private TextBlock rootBlock;
private final VersionedTextBlockNavigator navigator;
private Version activeVersion = Version.REFERENCE;
private boolean usecache = false;
public boolean isUsecache() {
return usecache;
}
public void setUsecache(boolean usecache) {
this.usecache = usecache;
}
public TextBlocksModel(TextBlock rootBlock) {
this(rootBlock, Version.REFERENCE);
}
public TextBlocksModel(TextBlock rootBlock2, Version activeVersion) {
this.activeVersion = activeVersion;
this.navigator = new VersionedTextBlockNavigator(activeVersion);
setRootTextBlock(rootBlock2);
}
public Version getActiveVersion() {
return activeVersion;
}
public void setActiveVersion(Version activeVersion) {
this.activeVersion = activeVersion;
}
public <T extends DocumentNode> T activeVersion(T node) {
return TbVersionUtil.getOtherVersion(node, activeVersion);
}
public TextBlock getRoot() {
return rootBlock;
}
/**
* Sets the root {@link TextBlock} to work on.
*
* @param rootBlock
*/
public void setRootTextBlock(TextBlock rootBlock) {
if (rootBlock == null) {
throw new IllegalArgumentException("null block passed as root");
}
if (rootBlock.getParent() != null) {
throw new IllegalArgumentException("block passed is not root");
}
this.rootBlock = rootBlock;
}
/**
* Returns the token at the offset, or the last token before the offset.
*
* @param offset
* absolute offset in text
* @return token at the offset, last toekn before the offset, or null, if no
* lexed tokens in textblocks model
* @throws TextBlocksModelException
*/
public AbstractToken getFloorTokenInRoot(int offset) {
return navigator.getFloorToken(rootBlock, offset);
}
/**
* @return length of the whole text
*/
public int getLength() {
return rootBlock.getLength();
}
/*
* Important: not based on rootBlock cachedString to allow the underlying
* textblocks model to be changed through other means than
* TextBlocksModel.replace().
*/
public char get(final int offset) {
if (offset < 0) { // should never happen
throw new IllegalArgumentException("Offset negative: " + offset);
}
if (offset >= getLength()) { // should never happen
throw new IllegalArgumentException("Offset outside text length " + offset + ">" + getLength());
}
if (usecache) {
return rootBlock.getCachedString().charAt(offset);
}
AbstractToken token = getFloorTokenInRoot(offset);
if (token == null) {
return ' ';
}
int tokenStartOffset = TbUtil.getAbsoluteOffset(token);
int tokenRelativeOffset = offset - tokenStartOffset;
String value = token.getValue();
if (tokenRelativeOffset < value.length()) {
return value.charAt(tokenRelativeOffset);
} else {
return ' ';
}
}
/*
* Important: not based on rootBlock cachedString to allow the underlying
* textblocks model to be changed through other means than
* TextBlocksModel.replace().
*/
public String get(final int regionOffset, final int regionLength) {
if (regionLength == 0) {
return "";
}
if (regionLength < 0) {
throw new IllegalArgumentException("regionLength negative");
}
if (regionOffset < 0) {
throw new IllegalArgumentException("regionOffset negative");
}
if (regionOffset > rootBlock.getLength()) {
throw new IllegalArgumentException("regionOffset larger than document length");
}
if (regionOffset + regionLength > rootBlock.getLength()) {
throw new IllegalArgumentException("regionLength too large");
}
// TODO: maybe check input and throw IllegalArgumentException
if (usecache) {
return rootBlock.getCachedString().substring(regionOffset, regionOffset + regionLength);
}
int remainingLength = regionLength;
StringBuilder result = new StringBuilder(regionLength);
// the position in the TextBlocksModel we currently look at for more
// chars
int currentAbsoluteCursorPosition = regionOffset;
// find entry covering the offset position
AbstractToken token = getFloorTokenInRoot(currentAbsoluteCursorPosition);
if (token == null) {
// start of region is before first token, need to fill blanks up to
// first token of until remaining length is full
while (remainingLength > 0 && token == null) {
result.append(' ');
currentAbsoluteCursorPosition++;
remainingLength--;
token = getFloorTokenInRoot(currentAbsoluteCursorPosition);
}
if (remainingLength == 0) {
return result.toString();
}
}
int entryOffset = getAbsoluteOffset(token); // entryOffset <=
// regionOffset
// the token-relative position we currently look at
int startOffsetInToken = currentAbsoluteCursorPosition - entryOffset;
while (remainingLength > 0) {
// in each step of the loop, we need to fill up the StringBuilder
// with
// token contents and blanks until remainingLength is zero.
String value = token.getValue();
// String resynchronizedValue =
// shortPrettyPrinter.resynchronizeToEditableState(token);
// if(value == null || "".equals(value)){
// //value is out of sync with model
// //update value from model if possible.
// value = resynchronizedValue;
// }
// if(token.getLength() != value.length()
// || (token.getValue() != null && !"".equals(value) &&
// !resynchronizedValue.equals(token.getValue()))) {
// // if(token.getVersion().equals(VersionEnum.PREVIOUS)) {
// //
// if(token.getValue().equals(TbVersionUtil.getOtherVersion(token,
// VersionEnum.REFERENCE).getValue())) {
// // replace(getAbsoluteOffset(token), token.getLength(),
// resynchronizedValue);
// // return get(regionOffset, regionLength);
// // } else {
// // //Value was manually changed so do not resynch!
// // }
// // } else {
// // replace(getAbsoluteOffset(token), token.getLength(),
// resynchronizedValue);
// // return get(regionOffset, regionLength);
// // }
// }
// if (token.getLength() != value.length()) { // Sanity check helps
// to detect bugs
// TextBlock workingCopy = (TextBlock)
// TbReplacingHelper.getOrCreateWorkingCopy(getRoot());
// replaceInNonEmptyTree(getAbsoluteOffset(token), value.length(),
// value, workingCopy);
// }
// how many chars of token can we use for the result
int resultPartLength = value.length() - startOffsetInToken;
if (resultPartLength <= 0) {
// This mean the current floor token ends before the startoffset
// we are interested in. We now need to
// add blanks until the next token, or remaining length is
// filled with blanks.
while (remainingLength > 0) {
result.append(' ');
currentAbsoluteCursorPosition++;
remainingLength--;
resultPartLength = 1;
AbstractToken newToken = getFloorTokenInRoot(currentAbsoluteCursorPosition);
if (newToken == null) {
throw new RuntimeException("No Floor token for position " + currentAbsoluteCursorPosition);
}
if (newToken != token) {
token = newToken;
entryOffset = getAbsoluteOffset(token);
startOffsetInToken = 0;
break;
}
}
continue;
// either remainingLength is zero, then the outer while will
// also stop
// or we have a new token
}
// check whether this is the last token we need to use, or whether
// we will need to look at more
if (remainingLength > resultPartLength) {
result.append(value.substring(startOffsetInToken, value.length()));
remainingLength = remainingLength - (resultPartLength);
currentAbsoluteCursorPosition = startOffsetInToken + entryOffset + resultPartLength;
// get next entry for next iteration
AbstractToken newToken = getFloorTokenInRoot(currentAbsoluteCursorPosition);
if (!newToken.equals(token)) {
token = newToken;
entryOffset = getAbsoluteOffset(token);
startOffsetInToken = 0; // from now on startOffset is always
// zero, always consider full token
// value
} else {
// will be dealt with in next iteration
startOffsetInToken += resultPartLength;
}
} else { // this is the last token to consider
result.append(value.substring(startOffsetInToken, startOffsetInToken + remainingLength));
break;
}
} // end while appending values
String resultString = result.toString();
// the following is just a workaround for inconsistent TextBlock Models
// after e.g. JmiExceptions.
if (resultString.length() != regionLength) {
throw new IllegalStateException("Bug: Resulting String '" + resultString + "' has inconsistent length "
+ regionLength + "!=" + resultString.length());
// int difference = originallength - resultString.length();
// if (difference > 0) {
// for (int i = 0; i < difference; i++) {
// resultString += '#';
// }
// } else {
// resultString = resultString.substring(0, originallength);
// }
}
return resultString;
}
/**
* As more than one token can be located within the replaced region, the
* situation is handled as follows: The first affected token has it's
* intersection with the replaced region replaced by the newText. The last
* affected token has it's intersection with the replaced region removed.
* All other affected nodes are removed from the textblocks model.
*
* Any of the updated tokens left with a length of 0 are removed from their
* parent textblock. Now empty textblocks are removed recursively as well.
* @see org.eclipse.jface.text.ITextStore#replace(int, int, java.lang.String)
*
*/
public void replace(final int replacedRegionOffset, final int replacedRegionLength, final String newText) {
TextBlock workingcopy = (TextBlock) TbReplacingHelper.getOrCreateWorkingCopy(rootBlock);
setRootTextBlock(workingcopy);
replace(workingcopy, replacedRegionOffset, replacedRegionLength, newText);
}
public ArrayList<TokenChange> doShortPrettyPrintToEditableVersion(ShortPrettyPrinter shortPrettyPrinter) {
ArrayList<TokenChange> updatedTokens = new ArrayList<TokenChange>();
AbstractToken tok = getStartToken();
while (tok != null && !(tok instanceof Eostoken)) {
if (tok instanceof LexedToken) {
String newValue = shortPrettyPrinter.resynchronizeToEditableState(tok);
// TODO check what to do with the empty string case!
if (newValue != null && !newValue.equals(tok.getValue()) && !newValue.equals("")) {
int length = tok.getLength();
int offset = TbUtil.getAbsoluteOffset(tok);
if (newValue.length() != length) {
replaceInNonEmptyTree(offset, length, newValue, rootBlock);
} else {
tok.setValue(newValue);
}
rootBlock.setCachedString(rootBlock.getCachedString().substring(0, offset) + newValue
+ rootBlock.getCachedString().substring(offset + length, rootBlock.getCachedString().length()));
updatedTokens.add(new TokenChange((LexedToken) tok, offset, length));
}
}
tok = TbNavigationUtil.nextToken(tok);
}
return updatedTokens;
}
private void replaceInNonEmptyTree(int replacedRegionAbsoluteOffset, int replacedRegionLength, String newText,
TextBlock workingCopy) {
// find out whether the egion starts on a token gap
DocumentNode bottomNode = navigator.getLeafNode(workingCopy, replacedRegionAbsoluteOffset);
if (bottomNode instanceof TextBlock) {
// extend a token in the block with blanks to cover the beginning of
// the gap.
TextBlock bottomBlock = (TextBlock) bottomNode;
if (newText.length() > 0) {
// prepare replacement by modifying the situation, prefer to
// extend the region over the previous token, if one exists
AbstractToken floorToken = navigator.getFloorToken(workingCopy, replacedRegionAbsoluteOffset);
if (floorToken != null) {
int floorTokenAbsoluteOffset = TbUtil.getAbsoluteOffset(floorToken);
// change replaced region text and offset / length to cover
// floor token
int distance = replacedRegionAbsoluteOffset - (floorTokenAbsoluteOffset + floorToken.getLength());
String gapBlanks = TbReplacingHelper.getGapBlanks(distance);
// now, replace whole floortoken up to end of replaced
// region
replacedRegionLength = floorToken.getLength() + distance + replacedRegionLength;
// replace with floortoken text, n blanks, and newText
newText = floorToken.getValue() + gapBlanks + newText;
// start replacing at floortoken
replacedRegionAbsoluteOffset = TbUtil.getAbsoluteOffset(floorToken);
} else {
/*
* get ceiling token from offset, which must be somewhere
* below the bottom block (else there'd be a floor token)
* actually the first non BOS token in root must be the next
* token, since there was no floor token. extend ceiling
* token and all its parents up to bottom block to cover
* replaced region
*/
AbstractToken ceilingToken = TbNavigationUtil.firstTokenWithoutBOS(bottomBlock);
TbReplacingHelper.extendLeftToOffsetAscending(ceilingToken, replacedRegionAbsoluteOffset);
}
}
}
// at this point it should hold that the tree is still consistent, and
// there exists a token at replacedRegionOffset.
CoverageBean rootCoverageType = new CoverageBean();
rootCoverageType.setCovered(true);
rootCoverageType.setNodeStartsLater(false);
rootCoverageType.setNodeEndsLater(true); // doesn't really matter
modifyBlockRecursive(workingCopy, replacedRegionAbsoluteOffset, replacedRegionLength, newText, rootCoverageType);
}
/**
* modifies this block and its subblocks recursively, assumes that the
* replacedRegionOffset is over a token, not at a token gap.
*
* @param currentBlock
* @param replacedRegionRelativeOffset
* relative to root offset
* @param replacedRegionLength
* @param newText
* @param coverageType
* @param bottomBlock
* null or the block in which the gap exists
* @return whether the traverse Block should be deleted
*/
private boolean modifyBlockRecursive(TextBlock currentBlock, int replacedRegionRelativeOffset, int replacedRegionLength,
String newText, CoverageBean coverageType) {
boolean shouldBeDeleted = false;
int currentBlockAbsoluteOffset = TbUtil.getAbsoluteOffset(currentBlock);
traverseSubTokens(currentBlock, replacedRegionRelativeOffset, replacedRegionLength, newText, currentBlockAbsoluteOffset);
traverseSubBlocks(currentBlock, replacedRegionRelativeOffset, replacedRegionLength, newText, currentBlockAbsoluteOffset);
if (TbNavigationUtil.getSubNodesSize(currentBlock) == 0) {
shouldBeDeleted = true;
// deal with root node special? (should never happen)
} else {
// modify offset and length of this textBlock
if (coverageType.isCovered()) { // should always be the case, else
// this method would not be entered
if (!coverageType.isNodeStartsLater() && !coverageType.isNodeEndsLater()) {
// textBlock starts earlier or at the same time, but ends
// before replacedregion end
// offset remains the same
// length may change to offsetInNode plus newText.length
int newBlocklength = replacedRegionRelativeOffset + newText.length(); // length
// change
// may
// be
// positive,
// negative
// or
// zero
if (newBlocklength != currentBlock.getLength()) {
currentBlock.setLength(newBlocklength);
}
} else if (coverageType.isNodeStartsLater() && coverageType.isNodeEndsLater()) { // textblock
// starts
// within
// region
// and
// ends
// outside
// length is reduced by overlap of region and block, since
// we cut off start of block
int overLapSize = (replacedRegionRelativeOffset + replacedRegionLength); // always
// positive
// in
// this
// case
if (overLapSize != 0) {
currentBlock.setLength(currentBlock.getLength() - overLapSize);
}
if (currentBlock.isOffsetRelative() && (replacedRegionRelativeOffset + currentBlock.getOffset()) < 0) {
// block relative offset moves by the region that will
// be cut off from its parent
// if replacement happens within parent, reduce the
// offset by some amount
// if replacement starts outside of parent, set offset
// to zero, as all siblings left of this token will be
// deleted else
currentBlock.setOffset(0);
} else {
// token absolute offset moves right by the amount cut
// off, but then also moves by the difference between
// original region and new region
int blockOffsetChange = overLapSize;
blockOffsetChange += (newText.length() - replacedRegionLength);
if (blockOffsetChange != 0) {
currentBlock.setOffset(currentBlock.getOffset() + blockOffsetChange);
}
}
} else { // textblock is within replaced region, but has a
// subBlock or token left at replacedRegionStart
// offset remains the same
// length may change by the difference of replaced
// region and new text
int regionLengthDifference = newText.length() - replacedRegionLength; // length
// change
// may
// be
// positive,
// negative
// or
// zero
if (regionLengthDifference != 0) {
currentBlock.setLength(currentBlock.getLength() + regionLengthDifference);
}
}
}
}
return shouldBeDeleted;
}
/**
* @param currentBlock
* @param replacedRegionRelativeOffset
* @param replacedRegionLength
* @param newText
* @param shouldBeDeleted
* @param currentBlockAbsoluteOffset
* @param bottomBlock
* @return
*/
private void traverseSubBlocks(TextBlock currentBlock, int replacedRegionRelativeOffset, int replacedRegionLength,
String newText, int currentBlockAbsoluteOffset) {
List<TextBlock> toBeDeleted = new ArrayList<TextBlock>();
List<TextBlock> subBlocks = currentBlock.getSubBlocks();
for (Iterator<TextBlock> iterator = subBlocks.iterator(); iterator.hasNext();) {
// TODO: to optimize performance, find (binary search) and start
// with first child covering region
TextBlock textBlock = iterator.next();
int blockRelativeOffset = textBlock.getOffset();
if (textBlock.isOffsetRelative() == false) {
blockRelativeOffset -= currentBlockAbsoluteOffset;
}
CoverageBean coverageType = CoverageBean.getCoverageBean(blockRelativeOffset,
blockRelativeOffset + textBlock.getLength(), replacedRegionRelativeOffset, replacedRegionRelativeOffset
+ replacedRegionLength);
if (coverageType.isCovered() == false) {
if (coverageType.isNodeStartsLater() == false) { // node is
// before
// replaced
// region
} else { // node is after replacement region, offset may change
if (textBlock.isOffsetRelative() && replacedRegionRelativeOffset < 0) {
// draw it to understand
int offsetDifference = replacedRegionRelativeOffset + replacedRegionLength;
if (offsetDifference != 0) {
textBlock.setOffset(textBlock.getOffset() - offsetDifference);
}
} else {
int offsetDifference = newText.length() - replacedRegionLength; // can
// be
// negative
// if
// newtext
// is
// shorter
if (offsetDifference != 0) {
textBlock.setOffset(textBlock.getOffset() + offsetDifference);
}
}
}
} else { // node covers replaced region
if (coverageType.isNodeRealInside()) {
toBeDeleted.add(textBlock);
} else {
textBlock.setChildrenChanged(true);
// change contents starting at offset
boolean needsDeleting = modifyBlockRecursive(textBlock, replacedRegionRelativeOffset - blockRelativeOffset,
replacedRegionLength, newText, coverageType);
if (needsDeleting) {
toBeDeleted.add(textBlock);
}
}
}
}
for (DocumentNode node : toBeDeleted) {
TbChangeUtil.delete(node);
}
}
/**
* @param current
* @param replacedRegionRelativeOffset
* @param replacedRegionLength
* @param newText
* @param currentBlockAbsoluteOffset
*/
private void traverseSubTokens(TextBlock current, int replacedRegionRelativeOffset, int replacedRegionLength, String newText,
int currentBlockAbsoluteOffset) {
List<AbstractToken> subtokens = current.getTokens();
List<AbstractToken> toBeDeleted = new ArrayList<AbstractToken>();
boolean isFirst = true;
for (Iterator<AbstractToken> iterator = subtokens.iterator(); iterator.hasNext();) {
// to optimize performance, find (binary search) and start with
// first child covering region
AbstractToken abstractToken = iterator.next();
if (!iterator.hasNext() && abstractToken instanceof Eostoken) {
int difference = newText.length() - replacedRegionLength;
abstractToken.setOffset(abstractToken.getOffset() + difference);
} else if (isFirst && abstractToken instanceof Bostoken) {
// do nothing ever
} else {
boolean needsDeleting = TbReplacingHelper.modifyTokenOnOverlap(abstractToken, currentBlockAbsoluteOffset,
replacedRegionRelativeOffset, replacedRegionLength, newText, /*shortPrettyPrinter*/ null);
if (needsDeleting) {
toBeDeleted.add(abstractToken);
}
}
isFirst = false;
}
for (DocumentNode node : toBeDeleted) {
TbChangeUtil.delete(node);
}
}
/**
* @param newText
* @param workingCopy
*/
private void replaceInEmptyTree(String newText, TextBlock workingCopy) {
workingCopy.setLength(newText.length());
if (workingCopy.getSubNodes().size() == 2) {
TbReplacingHelper.createInitialToken(workingCopy, newText);
} else { // subnodes size != 2
if (workingCopy.getSubNodes().size() != 3) {
throw new IllegalArgumentException(
"Method only works for textBlocks of length 0 if only BOS and EOS, or BOS, an empty token, and EOS are present.");
}
}
// change the middle token between BOS and EOS
AbstractToken tok = (AbstractToken) workingCopy.getSubNodes().get(1);
tok.setLength(newText.length());
tok.setValue(newText);
workingCopy.setLength(newText.length());
// set BOS offset
workingCopy.getTokens().get(workingCopy.getTokens().size() - 1).setOffset(newText.length());
}
/**
* replaces the content of a textBlock tree by modifying and deleting
* tokens. Assumes that root is passed in, and that there is a PREVIOUS
* version of root which contains PREVIOUS versions of all nodes. This
* method currently does not create any PREVIOUS versions of anything.
* Assumes all nodes (except root, BOS, EOS) have relative offsets !
*
* @param root
* @param replacedRegionAbsoluteOffset
* @param replacedRegionLength
* @param newText
*/
public void replace(final TextBlock root, final int replacedRegionAbsoluteOffset, final int replacedRegionLength, final String newText) {
TextBlock workingCopy = (TextBlock) TbReplacingHelper.getOrCreateWorkingCopy(root);
if (replacedRegionAbsoluteOffset < 0 || replacedRegionAbsoluteOffset > root.getLength()) {
throw new IllegalArgumentException(Integer.toString(replacedRegionLength));
}
if (replacedRegionAbsoluteOffset + replacedRegionLength > root.getLength()) {
throw new IllegalArgumentException((replacedRegionAbsoluteOffset + replacedRegionLength) + " > " + root.getLength());
}
if (root.getParent() != null) {
throw new IllegalArgumentException("TextBlock is not root.");
}
if (root.getLength() == 0) {
replaceInEmptyTree(newText, workingCopy);
} else {
replaceInNonEmptyTreeUsingSubDiffs(replacedRegionAbsoluteOffset, replacedRegionLength, newText, workingCopy);
}
workingCopy.setChildrenChanged(true);
TbReplacingHelper.updateBlockCachedString(workingCopy, replacedRegionAbsoluteOffset, replacedRegionLength, newText);
}
private void replaceInNonEmptyTreeUsingSubDiffs(final int replacedRegionAbsoluteOffset, final int replacedRegionLength,
final String newText, TextBlock workingCopy) {
TokenComparator currentContent = new TokenComparator(get(replacedRegionAbsoluteOffset, replacedRegionLength));
TokenComparator newContent = new TokenComparator(newText);
List<RangeDifference> diffs = Arrays.asList(RangeDifferencer.findDifferences(currentContent, newContent));
Collections.reverse(diffs);
for (RangeDifference diff : diffs) {
int diffStartNew = newContent.getTokenStart(diff.rightStart());
int diffEndNew = newContent.getTokenStart(diff.rightStart()+diff.rightLength());
int diffStartCurrent = currentContent.getTokenStart(diff.leftStart());
int diffEndCurrent = currentContent.getTokenStart(diff.leftStart()+diff.leftLength());
replaceInNonEmptyTree(replacedRegionAbsoluteOffset+diffStartCurrent, diffEndCurrent-diffStartCurrent,
newText.substring(diffStartNew, diffEndNew), workingCopy);
}
}
/**
* Computes a list of nodes containing the two leaf nodes corresponding to
* offsetFrom and offsetTo, and nodes between them. The nodes between the
* tokens are returned as a root set, meaning that the highest level nodes
* are returned, such that all model elements between the floor tokens are
* directly or indirectly (as children, children's children etc.) contained
* in the list, but the list has a minimum number of entries, given the
* above restrictions. This implies that it is not the absolute minimal root
* set, because the first and last leaf nodes are always included, even if
* the list could become smaller if some ancestor of them were instead
* included.
*
* Nodes in between the floor tokens are all right siblings of the left
* floor token, right siblings of the left floor token's parent etc and all
* left siblings of the right floor token and left siblings of the the right
* floor token's parent etc. If both tokens or the corresponding parents are
* in the same TextBlock, all siblings between the two nodes are added to
* the list.
*
* See test cases for examples.
*
* @param rootBlock
* the top-most TextBlock
* @param offsetFrom
* absolute start offset
* @param offsetTo
* absolute end offset
* @return minimal list of nodes that directly, or indirectly include all
* tokens between the floor tokens identified by offsetFrom and
* offsetTo
*/
public List<DocumentNode> getNodesBetweenAsRootSet(TextBlock rootBlock, int offsetFrom, int offsetTo) {
List<DocumentNode> results = new ArrayList<DocumentNode>();
if (offsetFrom < 0) {
throw new IllegalArgumentException("invalid region: offsetFrom is negative");
}
if (offsetTo < offsetFrom) {
throw new IllegalArgumentException("invalid region: offsetTo is smaller than offsetFrom");
}
if (offsetTo > TbUtil.getAbsoluteOffset(rootBlock) + rootBlock.getLength()) {
throw new IllegalArgumentException("Region outside textBlock : " + offsetTo + ">"
+ (TbUtil.getAbsoluteOffset(rootBlock) + rootBlock.getLength()));
}
// find leftmost leaf node of the region
DocumentNode leftLeafNode = navigator.getLeafNode(rootBlock, offsetFrom);
// find rightmost leaf node of the region
DocumentNode rightLeafNode = navigator.getLeafNode(rootBlock, offsetTo);
results.addAll(getNodesBetweenAsRootSet(leftLeafNode, rightLeafNode));
return results;
}
/**
* Computes a list of nodes containing the two floorTokens corresponding to
* offsetFrom and offsetTo, and nodes between them. The nodes between the
* tokens are returned as a root set, meaning that the highest level nodes
* are returned, such that all and only model elements between the tokens
* are directly or indirectly (as children, children's children etc.)
* contained in the list, but the list has a minimum number of entries. The
* list thus will never contain any 2 document nodes where one is an
* ancestor of the other.
*
* Nodes in between the floor tokens are all right siblings of the left
* floor token, right siblings of the left floor token's parent etc and all
* left siblings of the right floor token and left siblings of the the right
* floor token's parent etc. If both tokens or the corresponding parents are
* in the same TextBlock, all siblings between the two nodes are added to
* the list.
*
* See test cases for examples.
*
* @param rootBlock
* the top-most TextBlock
* @param offsetFrom
* absolute start offset
* @param offsetTo
* absolute end offset
* @return minimal list of nodes that directly, or indirectly include all
* tokens between the floor tokens identified by offsetFrom and
* offsetTo
*/
private List<DocumentNode> getNodesBetweenAsRootSet(DocumentNode leftFloorToken, DocumentNode rightFloorToken) {
List<DocumentNode> results = new ArrayList<DocumentNode>();
if (rightFloorToken == null || leftFloorToken == null) {
throw new IllegalArgumentException("Tokens must not be null");
}
if (leftFloorToken == rightFloorToken) {
// only one floor token
results.add(leftFloorToken);
return results;
}
results.add(leftFloorToken);
int leftNodeLevel = getLevel(leftFloorToken);
int rightNodeLevel = getLevel(rightFloorToken);
Map<Integer, DocumentNode> rightNodeLevelMap = TbUtil.createNodeLevelMap(rightFloorToken, rightNodeLevel);
DocumentNode curLeftNode = leftFloorToken;
int curLeftLevel = leftNodeLevel;
// get the node in the right node path which is on the same level as
// leftFloorNode (if any)
DocumentNode possibleSameLevelRightNode = rightNodeLevelMap.get(curLeftLevel);
// ascend
DocumentNode parentBlock = curLeftNode.getParent();
while (parentBlock != null) {
addRightSiblingsBeforeEndNode(results, curLeftNode, possibleSameLevelRightNode);
if (curLeftNode == null && possibleSameLevelRightNode == null ||
curLeftNode != null && possibleSameLevelRightNode != null &&
curLeftNode.getParent() == possibleSameLevelRightNode.getParent()) {
// found same level right node
if (possibleSameLevelRightNode == rightFloorToken) {
// we are done
results.add(rightFloorToken);
return results;
}
break;
}
curLeftNode = parentBlock;
parentBlock = parentBlock.getParent();
curLeftLevel--;
possibleSameLevelRightNode = rightNodeLevelMap.get(curLeftLevel);
}
// now either we have reached a level where both subtrees have a common
// parent, or we have reached root
DocumentNode curRightNode = possibleSameLevelRightNode;
int curRightLevel = curLeftLevel + 1;
DocumentNode nextRightNode = rightNodeLevelMap.get(curRightLevel);
// descend
while (nextRightNode != rightFloorToken) {
// if there are nodes left of current path node, include them
if (!isFirstInSubTree(nextRightNode)) {
DocumentNode firstSubNode = getSubNodeAt((TextBlock) curRightNode, 0);
results.add(firstSubNode);
addRightSiblingsBeforeEndNode(results, firstSubNode, nextRightNode);
}
curRightNode = nextRightNode;
curRightLevel++;
nextRightNode = rightNodeLevelMap.get(curRightLevel);
}
// no deeper parent, last block
if (!isFirstInSubTree(rightFloorToken)) {
DocumentNode firstSubNode = getSubNodeAt(rightFloorToken.getParent(), 0);
if (firstSubNode instanceof Bostoken) {
firstSubNode = getSubNodeAt(rightFloorToken.getParent(), 1);
}
results.add(firstSubNode);
addRightSiblingsBeforeEndNode(results, firstSubNode, rightFloorToken);
}
results.add(rightFloorToken);
return results;
}
/**
* Helper function that adds all nodes between start and end in the parent
* block's subNodes. If end is null, or not in the list of subNodes, add all
* remaining nodes.
*
* @param nodes
* nodes list to add to
* @param start
* node after which to start
* @param end
* node before which to end. can be null or not part of the
* subNodes
*/
public void addRightSiblingsBeforeEndNode(List<DocumentNode> nodes, DocumentNode start, DocumentNode end) {
if (start == end) {
return;
}
List<? extends DocumentNode> subNodes = getSubNodes(start.getParent());
int index = subNodes.indexOf(start) + 1;
while (index < subNodes.size()) {
DocumentNode nextNode = subNodes.get(index);
if (nextNode == end) {
return;
}
nodes.add(nextNode);
index++;
}
}
/**
*
*/
private Bostoken getStartToken() {
if (!(rootBlock.getSubNodes().get(0) instanceof Bostoken)) {
throw new IllegalStateException("TextBlocksModel is in illegal state, first token not BOS!");
}
return (Bostoken) rootBlock.getSubNodes().get(0);
}
}