/** * */ package com.sap.furcas.runtime.textblocks.modifcation; import static com.sap.furcas.runtime.textblocks.modifcation.TbChangeUtil.replaceTokenContents; import static com.sap.furcas.runtime.textblocks.modifcation.TbVersionUtil.getOtherVersion; import java.util.List; import org.eclipse.emf.ecore.EObject; import com.sap.furcas.metamodel.FURCAS.textblocks.AbstractToken; import com.sap.furcas.metamodel.FURCAS.textblocks.DocumentNode; import com.sap.furcas.metamodel.FURCAS.textblocks.LexedToken; import com.sap.furcas.metamodel.FURCAS.textblocks.TextBlock; import com.sap.furcas.metamodel.FURCAS.textblocks.TextblocksFactory; import com.sap.furcas.metamodel.FURCAS.textblocks.Version; import com.sap.furcas.runtime.textblocks.CoverageBean; import com.sap.furcas.runtime.textblocks.TbUtil; import com.sap.furcas.runtime.textblocks.shortprettyprint.ShortPrettyPrinter; /** * util for replacing regions within a textBlock Tree. * The class assumes a general textBlock tree with * potential gaps between textBlocks and tokens. * Any textblocktree has as a property that only token nodes are leaves. * Also, the root is always a TextBlock, always has BOS and EOS as first and lat token, * and always additionally has a token or textBlock between BOS and EOS, which * might be an empty token. But only root may have a token of length 0, and only if root.length == 0; * * In a Tree without gaps it would hold that the absolute * offset of each first child is the same as its parent, the * root offset is zero, for each subnode. offset+length == nextSibling.offset * and that last subnodes offset+length is equal to its parent offset + length. * In a textBlock with gaps all the equals relations are replaced by <= relations. * * As a result, in a tree without gaps, there is a token for any position * between 0 and the length of root, while with gaps there may be positions * not covered by tokens. * * However, we still assume the root block to start at the absolute index 0. */ public class TbReplacingHelper { /* * Implementation details: * The implementation runs by traversing the textblock tree and checking for * each node whether it lies ahead, after, or overlapping with the replaced region. * Depending on whether the node is a token or a textBlock, different actions are taken. * Any node which is completely left of the replaced region is left untouched, and * children of such nodes are not traversed (which would cause problems for absolute offsets). * * Any other node will have its offset, length, and content replaced, or it will * be deleted if it would be reduced to length 0. Again, for nodes after the replaced * region, subnodes are not traversed since their relative offsets must remain the same. * * Since this class does not create any new Token unless for an empty TextBlock Tree, * it will always attempt to reuse an existing token to fill the new content in. * */ /** * Retrieves a {@link VersionEnum#PREVIOUS} version of the given node by * either returning an exisiting one or if none exist by firstly * initializing a working copy and returning it then. * * @param node * @return the {@link VersionEnum#PREVIOUS} version of the node */ public static DocumentNode getOrCreateWorkingCopy(DocumentNode node) { if( getOtherVersion(node,Version.PREVIOUS) == null) { TbUtil.createNewCopy(node, Version.PREVIOUS, false, null); } DocumentNode previousVersion = getOtherVersion(node, Version.PREVIOUS); // TODO this might have to be changed if the textblock elements // should be assignable // to a custom partition // FIXME: how are we supposed to handle this? if (((EObject) node).eResource() != null) { ((EObject) node).eResource().getContents().add(previousVersion); } return previousVersion; } /** * changes the absolute offset of token to new absoluteOffset by adding blanks on the left. * Assumes there are no tokens between given offset and node offset. * assumes offsets of non root nodes are relative. * assumes replacedRegionOffset >= 0 * @param node * @param replacedRegionRelativeOffset */ public static void extendLeftToOffsetAscending(DocumentNode node, int replacedRegionAbsoluteOffset) { int absoluteOffset = TbUtil.getAbsoluteOffset(node); if (absoluteOffset > replacedRegionAbsoluteOffset) { int difference = absoluteOffset - replacedRegionAbsoluteOffset; // always positive node.setLength(node.getLength() + difference); int relativeOffset = node.getOffset(); if (node instanceof AbstractToken) { AbstractToken token = (AbstractToken) node; token.setValue(getGapBlanks(difference) + token.getValue()); } if (relativeOffset <= difference) { node.setOffset(0); extendLeftToOffsetAscending(node.getParent(), replacedRegionAbsoluteOffset); } else { node.setOffset(node.getOffset() - difference); } } } /** * @param i * @return */ public static String getGapBlanks(int length) { StringBuilder buffer = new StringBuilder(length); for (int j = 0; j < length; j++) { buffer.append(' '); } return buffer.toString(); } /** * creates a new token, assumes there is only root, EOS, BOS in the tree. * @param firstAffected * @param newText * @param replacedRegionOffset */ public static void createInitialToken(TextBlock target, String newText) { LexedToken newToken = TextblocksFactory.eINSTANCE.createLexedToken(); // make the same version as the rootBlock newToken.setVersion(target.getVersion()); newToken.setValue(newText); newToken.setLength(newText.length()); newToken.setOffset(0); newToken.setOffsetRelative(true); List<DocumentNode> tokens = target.getSubNodes(); int index = 1; // insert after BOS, before EOS // for (Iterator<AbstractToken> iterator = tokens.iterator(); iterator.hasNext();) { // AbstractToken abstractToken = (AbstractToken) iterator.next(); // if (abstractToken.getOffset() > 0 || abstractToken instanceof Eostoken) { // break; // } // index++; // } tokens.add(index, newToken); /*AbstractToken workingCopy = (AbstractToken) */ getWorkingCopy(newToken); } /** * @param abstractToken * @param newText * @return whether the token should be deleted */ public static boolean modifyTokenOnOverlap(AbstractToken abstractToken, int parentBlockAbsoluteOffset, int replacedRegionRelativeOffset, int replacedRegionLength, String newText, ShortPrettyPrinter shortPrettyPrinter) { boolean shouldBeDeleted = false; int tokenRelativeOffset = abstractToken.getOffset(); if (abstractToken.isOffsetRelative() == false) { tokenRelativeOffset -= parentBlockAbsoluteOffset; } CoverageBean coverageType = CoverageBean.getCoverageBean(tokenRelativeOffset, tokenRelativeOffset+abstractToken.getLength(), replacedRegionRelativeOffset, replacedRegionRelativeOffset + replacedRegionLength ); if (coverageType.isCovered() == false) { if (coverageType.isNodeStartsLater() == false) { // do nothing } else { // node is after replacement region, offset may change // change depends on overlap of region with parent if (abstractToken.isOffsetRelative() && replacedRegionRelativeOffset < 0) { // draw it to understand int offsetDifference = replacedRegionRelativeOffset + replacedRegionLength; if (offsetDifference != 0) { abstractToken.setOffset(abstractToken.getOffset() - offsetDifference); } } else { int offsetDifference = newText.length() - replacedRegionLength; // can be negative if newtext is shorter if (offsetDifference != 0) { abstractToken.setOffset(abstractToken.getOffset() + offsetDifference); } } } } else { // token is covered if (coverageType.isNodeRealInside() ) { // token must be deleted shouldBeDeleted = true; } else if (coverageType.isNodeStartsLater() ) { // need to cut off from the start of this token AbstractToken workingCopy = getWorkingCopy(abstractToken); /**TODO begin view hack**/ if(workingCopy == null) { workingCopy = abstractToken; } /**TODO end start view hack**/ // if token does not fully cover region, remove the left characters covered by the region int overlapLength = replacedRegionRelativeOffset + replacedRegionLength - abstractToken.getOffset(); TbChangeUtil.replaceTokenContents(workingCopy, "", 0, overlapLength, shortPrettyPrinter); if (workingCopy.getLength() == 0) { // did cut off delete whole token? (should never happen, as this is really inside coverage) shouldBeDeleted = true; } else { if (abstractToken.isOffsetRelative() && replacedRegionRelativeOffset < 0) { // token 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 abstractToken.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 tokenOffsetChange = overlapLength; tokenOffsetChange += (newText.length() - replacedRegionLength); if (tokenOffsetChange != 0) { abstractToken.setOffset(abstractToken.getOffset() + tokenOffsetChange); } } } } else { // need to replace parts of this token AbstractToken workingCopy = getWorkingCopy(abstractToken ); /**TODO begin view hack**/ if(workingCopy == null) { workingCopy = abstractToken; } /**TODO end start view hack**/ int replacedRegionOffsetTo = replacedRegionRelativeOffset + replacedRegionLength; int tokenEndOffset = tokenRelativeOffset + workingCopy.getLength(); int replaceInTokenLength1 = Math.min(replacedRegionOffsetTo, tokenEndOffset) - replacedRegionRelativeOffset; // either replace all or replace a part of the middle int offsetInTokenFrom = replacedRegionRelativeOffset - tokenRelativeOffset; // always positive // replace intersecting region in token with complete new text replaceTokenContents(workingCopy, newText, offsetInTokenFrom, replaceInTokenLength1, shortPrettyPrinter); if (workingCopy.getLength() == 0) { shouldBeDeleted = true; } } // end if token overlaps need to cut off left or right } // endif token is covered return shouldBeDeleted; } /** * Retrieves a {@link VersionEnum#PREVIOUS} version of the given node by * either returning an exisiting one or if none exist by firstly * initializing a working copy and returning it then. * * @param node * @return the {@link VersionEnum#PREVIOUS} version of the node */ public static <Type extends DocumentNode> Type getWorkingCopy(Type node) { return getOtherVersion(node, Version.PREVIOUS); } public static void updateBlockCachedString(TextBlock rootBlock, int replacedRegionOffset, int replacedRegionLength, String newText) { // TODO: currently needed because linetracker relies on it // check how and when to remove this rootBlock.setCachedString(rootBlock.getCachedString().substring(0, replacedRegionOffset) + newText + rootBlock.getCachedString().substring( replacedRegionOffset + replacedRegionLength, rootBlock.getCachedString().length())); } }