| /******************************************************************************* |
| * Copyright (c) 2001, 2006 IBM Corporation and others. |
| * 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: |
| * IBM Corporation - initial API and implementation |
| * Jens Lukowski/Innoopract - initial renaming/restructuring |
| * |
| *******************************************************************************/ |
| package org.eclipse.wst.sse.core.internal.text; |
| |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.FindReplaceDocumentAdapter; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.wst.sse.core.internal.Logger; |
| import org.eclipse.wst.sse.core.internal.ltk.parser.BlockTagParser; |
| import org.eclipse.wst.sse.core.internal.provisional.events.NoChangeEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.RegionChangedEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.RegionsReplacedEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.events.StructuredDocumentRegionsReplacedEvent; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredTextReParser; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegion; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionCollection; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionContainer; |
| import org.eclipse.wst.sse.core.internal.provisional.text.ITextRegionList; |
| import org.eclipse.wst.sse.core.internal.util.Debug; |
| import org.eclipse.wst.sse.core.internal.util.Utilities; |
| import org.eclipse.wst.sse.core.utils.StringUtils; |
| |
| |
| /** |
| * This class provides a centralized place to put "reparsing" logic. This is |
| * the logic that reparses the text incrementally, as a user types in new |
| * characters, or DOM nodes are inserted or deleted. Note: it is not a thread |
| * safe class. |
| */ |
| public class StructuredDocumentReParser implements IStructuredTextReParser { |
| protected IStructuredDocumentRegion dirtyEnd = null; |
| protected IStructuredDocumentRegion dirtyStart = null; |
| final private String doubleQuote = new String(new char[]{'\"'}); |
| protected final CoreNodeList EMPTY_LIST = new CoreNodeList(); |
| protected String fChanges; |
| protected String fDeletedText; |
| |
| private FindReplaceDocumentAdapter fFindReplaceDocumentAdapter = null; |
| protected int fLengthDifference; |
| protected int fLengthToReplace; |
| protected Object fRequester; |
| protected int fStart; |
| // note: this is the impl class of IStructuredDocument, not the interface |
| // FUTURE_TO_DO: I believe some of these can be made private now.? |
| protected BasicStructuredDocument fStructuredDocument; |
| |
| /** |
| * variable used in anticiapation of multithreading |
| */ |
| protected boolean isParsing; |
| final private String singleQuote = new String(new char[]{'\''}); |
| |
| public StructuredDocumentReParser() { |
| super(); |
| } |
| |
| public StructuredDocumentEvent _checkBlockNodeList(List blockTagList) { |
| StructuredDocumentEvent result = null; |
| if (blockTagList != null) { |
| for (int i = 0; i < blockTagList.size(); i++) { |
| org.eclipse.wst.sse.core.internal.ltk.parser.BlockMarker blockTag = (org.eclipse.wst.sse.core.internal.ltk.parser.BlockMarker) blockTagList.get(i); |
| String tagName = blockTag.getTagName(); |
| result = checkForCriticalName("<" + tagName); //$NON-NLS-1$ |
| if (result != null) |
| break; |
| result = checkForCriticalName("</" + tagName); //$NON-NLS-1$ |
| if (result != null) |
| break; |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Common utility for checking for critical word such as " <SCRIPT>" |
| */ |
| private StructuredDocumentEvent _checkForCriticalWord(String criticalTarget, boolean checkEnd) { |
| StructuredDocumentEvent result = null; |
| int documentLength = fStructuredDocument.getLength(); |
| int propLen = fLengthToReplace; |
| if (propLen > documentLength) |
| propLen = documentLength; |
| int startNeighborhood = fStart - criticalTarget.length(); |
| int adjustInsert = 0; |
| if (startNeighborhood < 0) { |
| adjustInsert = 0 - startNeighborhood; |
| startNeighborhood = 0; |
| } |
| int endNeighborhood = fStart + fLengthToReplace + criticalTarget.length() - 1; |
| if (endNeighborhood > documentLength) |
| endNeighborhood = documentLength - 1; |
| int oldlen = endNeighborhood - startNeighborhood; // + 1; |
| if (oldlen + startNeighborhood > documentLength) { |
| oldlen = documentLength - startNeighborhood; |
| } |
| String oldText = fStructuredDocument.get(startNeighborhood, oldlen); |
| String peek = StringUtils.paste(oldText, fChanges, criticalTarget.length() - adjustInsert, fLengthToReplace); |
| boolean isCriticalString = checkTagNames(oldText, criticalTarget, checkEnd); |
| boolean toBeCriticalString = checkTagNames(peek, criticalTarget, checkEnd); |
| if ((isCriticalString != toBeCriticalString) || // OR if both are |
| // critical and there's |
| // a change in the end |
| // tag ('>') |
| ((isCriticalString && toBeCriticalString) && (changeInIsEndedState(oldText, peek)))) { |
| // if it involves a change of a critical string (making one where |
| // there wasn't, or removing |
| // one where there was one) then reparse everthing. |
| result = reparse(0, documentLength - 1); |
| } |
| return result; |
| } |
| |
| private int _computeStartOfDifferences(CoreNodeList oldNodes, CoreNodeList newNodes) { |
| int startOfDifferences = -1; |
| int newNodesLength = newNodes.getLength(); |
| boolean foundDifference = false; |
| boolean done = false; |
| // we'll control our loop based on the old List length |
| int oldNodesLength = oldNodes.getLength(); |
| // be sure to check 'done' first, so startOfDifferences isn't |
| // icremented if done is true |
| done : while ((!done) && (++startOfDifferences < oldNodesLength)) { |
| IStructuredDocumentRegion oldNode = oldNodes.item(startOfDifferences); |
| // this lessThanEffectedRegion is to check to be sure the node is |
| // infact a candidate |
| // to be considered as "old". This check is important for the case |
| // where some |
| // text is replaceing text that |
| // appears identical, but is a different instance. For example, if |
| // the text |
| // is <P><B></B></P> and <B></B> is inserted at postion 3, |
| // resulting in <P><B></B><B></B></P> |
| // we do not want the |
| // first <B> to be considered old ... it is the new one, the |
| // second |
| // <B> is the old one. |
| if (_lessThanEffectedRegion(oldNode)) { |
| // be sure to check that we have new nodes to compare against. |
| if (startOfDifferences > newNodesLength) { |
| foundDifference = false; |
| done = true; |
| continue done; |
| } else { |
| // |
| IStructuredDocumentRegion newNode = newNodes.item(startOfDifferences); |
| // note: shift is 0 while at beginning of list, before the |
| // insertion (or deletion) point. After that, it is |
| // fStart+fLengthDifference |
| if (!(oldNode.sameAs(newNode, 0))) { |
| foundDifference = true; |
| done = true; |
| continue done; |
| } else { // if they are equal, then we will be keeping the |
| // old one, so |
| // we need to be sure its parentDocument is set back |
| // to |
| // the right instance |
| oldNode.setParentDocument(fStructuredDocument); |
| } |
| } |
| } else { |
| // we didn't literally find a difference, but we count it as |
| // such by implication |
| foundDifference = true; |
| done = true; |
| continue done; |
| } |
| } |
| // if we literally found a difference, then all is ok and we can |
| // return |
| // it. |
| // if we did not literally find one, then we have to decide why. |
| if (!foundDifference) { |
| if (newNodesLength == oldNodesLength) { // then lists are |
| // identical |
| // (and may be of zero |
| // length) |
| startOfDifferences = -1; |
| } else { |
| if (newNodesLength > oldNodesLength) { // then lists are |
| // identical except for |
| // newNodes added |
| startOfDifferences = oldNodesLength; |
| } else { |
| if (newNodesLength < oldNodesLength) { // then lists are |
| // identical except |
| // for old Nodes |
| // deleted |
| startOfDifferences = newNodesLength; |
| } |
| } |
| } |
| } |
| return startOfDifferences; |
| } |
| |
| private int _computeStartOfDifferences(IStructuredDocumentRegion oldNodeParam, ITextRegionList oldRegions, IStructuredDocumentRegion newNodeParam, ITextRegionList newRegions) { |
| int startOfDifferences = -1; |
| int newRegionsLength = newRegions.size(); |
| boolean foundDifference = false; |
| boolean done = false; |
| // we'll control our loop based on the old List length |
| int oldRegionsLength = oldRegions.size(); |
| // be sure to check 'done' first, so startOfDifferences isn't |
| // icremented if done is true |
| done : while ((!done) && (++startOfDifferences < oldRegionsLength)) { |
| ITextRegion oldRegion = oldRegions.get(startOfDifferences); |
| // this lessThanEffectedRegion is to check to be sure the node is |
| // infact a candidate |
| // to be considered as "old". This check is important for the case |
| // where some |
| // text is replaceing text that |
| // appears identical, but is a different instance. For example, if |
| // the text |
| // is <P><B></B></P> and <B></B> is inserted at postion 3, |
| // resulting in <P><B></B><B></B></P> |
| // we do not want the |
| // first <B> to be considered old ... it is the new one, the |
| // second |
| // <B> is the old one. |
| if (_lessThanEffectedRegion(oldNodeParam, oldRegion)) { |
| // be sure to check that we have new nodes to compare against. |
| if (startOfDifferences > newRegionsLength) { |
| foundDifference = false; |
| done = true; |
| continue done; |
| } else { |
| // |
| ITextRegion newRegion = newRegions.get(startOfDifferences); |
| // note: shift is 0 while at beginning of list, before the |
| // insertion (or deletion) point. After that, it is |
| // fStart+fLengthDifference |
| if (!(oldNodeParam.sameAs(oldRegion, newNodeParam, newRegion, 0))) { |
| foundDifference = true; |
| done = true; |
| continue done; |
| } else { |
| // if they are equal, then we will be keeping the old |
| // one. |
| // unlike the flatnode case, there is no reason to |
| // update |
| // the textstore, since its the same text store in |
| // either case |
| // (since its the same flatnode) |
| //oldRegion.setTextStore(fStructuredDocument.parentDocument); |
| } |
| } |
| } else { |
| // we didn't literally find a difference, but we count it as |
| // such by implication |
| foundDifference = true; |
| done = true; |
| continue done; |
| } |
| } |
| // if we literally found a difference, then all is ok and we can |
| // return |
| // it. |
| // if we did not literally find one, then we have to decide why. |
| if (!foundDifference) { |
| if (newRegionsLength == oldRegionsLength) { // then lists are |
| // identical (and may |
| // be of zero length) |
| startOfDifferences = -1; |
| } else { |
| if (newRegionsLength > oldRegionsLength) { // then lists are |
| // identical except |
| // for newRegions |
| // added |
| startOfDifferences = oldRegionsLength; |
| } else { |
| if (newRegionsLength < oldRegionsLength) { // then lists |
| // are identical |
| // except for |
| // old Nodes |
| // deleted |
| startOfDifferences = newRegionsLength; |
| } |
| } |
| } |
| } |
| return startOfDifferences; |
| } |
| |
| /** |
| * Part 1 of 2 steps to do a core_reparse |
| * |
| * Parses a portion of the current text in the IStructuredDocument and |
| * returns the raw result |
| */ |
| private IStructuredDocumentRegion _core_reparse_text(int rescanStart, int rescanEnd) { |
| fStructuredDocument.resetParser(rescanStart, rescanEnd); |
| return fStructuredDocument.getParser().getDocumentRegions(); |
| } |
| |
| /** |
| * Part 2 of 2 steps to do a core_reparse |
| * |
| * Integrates a list of StructuredDocumentRegions based on the current |
| * text contents of the IStructuredDocument into the IStructuredDocument |
| * data structure |
| */ |
| private StructuredDocumentEvent _core_reparse_update_model(IStructuredDocumentRegion newNodesHead, int rescanStart, int rescanEnd, CoreNodeList oldNodes, boolean firstTime) { |
| StructuredDocumentEvent result = null; |
| CoreNodeList newNodes = null; |
| // rescan |
| newNodes = new CoreNodeList(newNodesHead); |
| // adjust our newNode chain so the offset positions match |
| // our text store (not the simple string of text reparsed) |
| StructuredDocumentRegionIterator.adjustStart(newNodesHead, rescanStart); |
| // initialize the parentDocument variable of each instance in the new |
| // chain |
| StructuredDocumentRegionIterator.setParentDocument(newNodesHead, fStructuredDocument); |
| // initialize the structuredDocument variable of each instance in the |
| // new chain |
| //StructuredDocumentRegionIterator.setStructuredDocument(newNodesHead, |
| // fStructuredDocument); |
| // |
| if (firstTime) { |
| fStructuredDocument.setCachedDocumentRegion(newNodesHead); |
| fStructuredDocument.initializeFirstAndLastDocumentRegion(); |
| // note: since we are inserting nodes, for the first time, there |
| // is |
| // no adjustments |
| // to downstream stuff necessary. |
| result = new StructuredDocumentRegionsReplacedEvent(fStructuredDocument, fRequester, oldNodes, newNodes, fChanges, fStart, fLengthToReplace); |
| } else { |
| // note: integrates changes into model as a side effect |
| result = minimumEvent(oldNodes, newNodes); |
| } |
| result.setDeletedText(fDeletedText); |
| return result; |
| } |
| |
| private CoreNodeList _formMinimumList(CoreNodeList flatnodes, int startOfDifferences, int endOfDifferences) { |
| CoreNodeList minimalNodes = null; |
| // if startOfDifferces is still its initial value, then we have an |
| // empty document |
| if (startOfDifferences == -1) { |
| minimalNodes = EMPTY_LIST; |
| } else { |
| // if we do not have any flatnode in our flatnode list, then |
| // simply |
| // return our standard empty list |
| if (flatnodes.getLength() == 0) { |
| minimalNodes = EMPTY_LIST; |
| } else { |
| // if startOfDifferences is greater than endOfDifferences, |
| // then |
| // that means the calculations "crossed" each other, and |
| // hence, |
| // there really is no differences, so, again, return the empty |
| // list |
| if (startOfDifferences > endOfDifferences) { |
| minimalNodes = EMPTY_LIST; |
| } else { |
| // the last check be sure we have some differnces |
| if ((endOfDifferences > -1)) { |
| minimalNodes = new CoreNodeList(flatnodes.item(startOfDifferences), flatnodes.item(endOfDifferences)); |
| } else { |
| // there were no differences, the list wasn't |
| // minimized, so simply return it. |
| minimalNodes = flatnodes; |
| } |
| } |
| } |
| } |
| return minimalNodes; |
| } |
| |
| private boolean _greaterThanEffectedRegion(IStructuredDocumentRegion oldNode) { |
| boolean result = false; |
| int nodeStart = oldNode.getStartOffset(); |
| int changedRegionEnd = fStart + fLengthToReplace - 1; |
| result = nodeStart > changedRegionEnd; |
| return result; |
| } |
| |
| private boolean _greaterThanEffectedRegion(IStructuredDocumentRegion oldNode, ITextRegion oldRegion) { |
| boolean result = false; |
| int regionStartOffset = oldNode.getStartOffset(oldRegion); |
| int effectedRegionEnd = fStart + fLengthToReplace - 1; |
| result = regionStartOffset > effectedRegionEnd; |
| return result; |
| } |
| |
| private boolean _lessThanEffectedRegion(IStructuredDocumentRegion oldNode) { |
| boolean result = false; |
| int nodeEnd = oldNode.getEndOffset() - 1; |
| result = nodeEnd < fStart; |
| return result; |
| } |
| |
| private boolean _lessThanEffectedRegion(IStructuredDocumentRegion oldNode, ITextRegion oldRegion) { |
| boolean result = false; |
| int nodeEnd = oldNode.getEndOffset(oldRegion) - 1; |
| result = nodeEnd < fStart; |
| return result; |
| } |
| |
| private boolean _regionsSameKind(ITextRegion newRegion, ITextRegion oldRegion) { |
| boolean result = false; |
| // if one region is a container region, and the other not, always |
| // return false |
| // else, just check their type. |
| // DW druing refactoring, looks like a "typo" here, using 'old' in |
| // both. |
| // if (isContainerRegion(oldRegion) != isContainerRegion(oldRegion)) |
| if (isCollectionRegion(oldRegion) != isCollectionRegion(newRegion)) |
| result = false; |
| else if (oldRegion.getType() == newRegion.getType()) |
| result = true; |
| return result; |
| } |
| |
| // private boolean hasCollectionRegions(ITextRegion aRegion) { |
| // boolean result = false; |
| // if (aRegion instanceof ITextRegionCollection) { |
| // ITextRegionCollection regionContainter = (ITextRegionCollection) |
| // aRegion; |
| // ITextRegionList regions = regionContainter.getRegions(); |
| // Iterator iterator = regions.iterator(); |
| // while (iterator.hasNext()) { |
| // if (aRegion instanceof ITextRegionCollection) { |
| // result = true; |
| // break; |
| // } |
| // } |
| // } |
| // return result; |
| // } |
| /** |
| * This method is specifically to detect changes in 'isEnded' state, |
| * although it still does so with heuristics. If number of '>' changes, |
| * assume the isEnded state has changed. |
| */ |
| private boolean changeInIsEndedState(String oldText, String newText) { |
| int nOld = StringUtils.occurrencesOf(oldText, '>'); |
| int nNew = StringUtils.occurrencesOf(newText, '>'); |
| return !(nOld == nNew); |
| } |
| |
| private void checkAndAssignParent(IStructuredDocumentRegion oldNode, ITextRegion region) { |
| if (region instanceof ITextRegionContainer) { |
| ((ITextRegionContainer) region).setParent(oldNode); |
| return; |
| } |
| if (region instanceof ITextRegionCollection) { |
| ITextRegionCollection textRegionCollection = (ITextRegionCollection) region; |
| ITextRegionList regionList = textRegionCollection.getRegions(); |
| for (int i = 0; i < regionList.size(); i++) { |
| ITextRegion innerRegion = regionList.get(i); |
| checkAndAssignParent(oldNode, innerRegion); |
| } |
| } |
| } |
| |
| /** |
| * A change to a CDATA tag can result in all being reparsed. |
| */ |
| private StructuredDocumentEvent checkForCDATA() { |
| StructuredDocumentEvent result = null; |
| result = checkForCriticalKey("<![CDATA["); //$NON-NLS-1$ |
| if (result == null) |
| result = checkForCriticalKey("]]>"); //$NON-NLS-1$ |
| return result; |
| } |
| |
| /** |
| * If a comment start or end tag is being added or deleted, we'll rescan |
| * the whole document. The reason is that content that is revealed or |
| * commented out can effect the interpretation of the rest of the |
| * document. Note: for now this is very XML specific, can refactor/improve |
| * later. |
| */ |
| protected StructuredDocumentEvent checkForComments() { |
| StructuredDocumentEvent result = null; |
| result = checkForCriticalKey("<!--"); //$NON-NLS-1$ |
| if (result == null) |
| result = checkForCriticalKey("-->"); //$NON-NLS-1$ |
| // we'll also check for these degenerate cases |
| if (result == null) |
| result = checkForCriticalKey("<!--->"); //$NON-NLS-1$ |
| return result; |
| } |
| |
| /** |
| * Common utility for checking for critical word such as " <SCRIPT>" |
| */ |
| protected StructuredDocumentEvent checkForCriticalKey(String criticalTarget) { |
| return _checkForCriticalWord(criticalTarget, false); |
| } |
| |
| /** |
| * Common utility for checking for critical word such as " <SCRIPT>" |
| */ |
| private StructuredDocumentEvent checkForCriticalName(String criticalTarget) { |
| return _checkForCriticalWord(criticalTarget, true); |
| } |
| |
| // /** |
| // * Currently this method is pretty specific to ?ML |
| // * @deprecated - not really deprecated, but plan to make |
| // * protected ... I'm not sure why its public or misspelled? |
| // */ |
| protected StructuredDocumentEvent checkForCrossStructuredDocumentRegionBoundryCases() { |
| StructuredDocumentEvent result = null; |
| // Case 1: See if the language's syntax requires that multiple |
| // StructuredDocumentRegions be rescanned |
| if (result == null) { |
| result = checkForCrossStructuredDocumentRegionSyntax(); |
| } |
| // Case 2: "block tags" whose content is left unparsed |
| if (result == null) { |
| Object parser = fStructuredDocument.getParser(); |
| if (parser instanceof BlockTagParser) { |
| List blockTags = ((BlockTagParser) parser).getBlockMarkers(); |
| result = _checkBlockNodeList(blockTags); |
| } |
| } |
| // FUTURE_TO_DO: is there a better place to do this? |
| // or! do we already do it some other more central place? |
| if (result != null) { |
| result.setDeletedText(fDeletedText); |
| } |
| return result; |
| } |
| |
| /** |
| * Allow a reparser to check for extra syntactic cases that require |
| * parsing beyond the flatNode boundary. |
| * |
| * This implementation is very XML-centric. |
| */ |
| protected StructuredDocumentEvent checkForCrossStructuredDocumentRegionSyntax() { |
| StructuredDocumentEvent result; |
| // Case 1: Quote characters are involved |
| result = checkForQuotes(); |
| if (result == null) { |
| // Case 2: The input forms or undoes a comment beginning or |
| // comment |
| // end |
| result = checkForComments(); |
| } |
| if (result == null) { |
| // Case 3: The input forms or undoes a processing instruction |
| result = checkForPI(); |
| } |
| if (result == null) { |
| // Case 4: The input forms or undoes a CDATA section |
| result = checkForCDATA(); |
| } |
| return result; |
| } |
| |
| /** |
| * Checks to see if change request exactly matches the text it would be |
| * replacing. (In future, this, or similar method is where to check for |
| * "read only" attempted change.) |
| */ |
| private StructuredDocumentEvent checkForNoChange() { |
| StructuredDocumentEvent result = null; |
| // don't check equals unless lengths match |
| // should be a tiny bit faster, since usually not |
| // of equal lengths (I'm surprised String's equals method |
| // doesn't do this.) |
| if ((fChanges != null) && (fDeletedText != null) && (fChanges.length() == fDeletedText.length()) && (fChanges.equals(fDeletedText))) { |
| result = new NoChangeEvent(fStructuredDocument, fRequester, fChanges, fStart, fLengthToReplace); |
| } |
| return result; |
| } |
| |
| /** |
| * A change to a PI tag can result in all being reparsed. |
| */ |
| private StructuredDocumentEvent checkForPI() { |
| StructuredDocumentEvent result = null; |
| result = checkForCriticalKey("<?"); //$NON-NLS-1$ |
| if (result == null) |
| result = checkForCriticalKey("?>"); //$NON-NLS-1$ |
| return result; |
| } |
| |
| // For simplicity, if either text to be deleted, or text to be inserted |
| // contains at least |
| // one quote, we'll search for previous quote in document, if any, and use |
| // that flatnode as |
| // a dirty start, and we'll use end of document as dirty end. We need to |
| // assume either \" or |
| // \' is an exceptable quote. (NOTE: this is, loosely, an XML assumption |
| // -- |
| // other languages |
| // would differ, but we'll "hard code" for XML for now. |
| // future_TODO: this is a really bad heuristic ... we should be looking |
| // for |
| // odd number of quotes |
| // within a structuredDocumentRegion (or something!) This causes way too |
| // much reparsing on |
| // simple cases, like deleting a tag with a quoted attribute! |
| private StructuredDocumentEvent checkForQuotes() { |
| // routine is supported with null or empty string meaning the same |
| // thing: deletion |
| if (fChanges == null) |
| fChanges = ""; //$NON-NLS-1$ |
| // |
| StructuredDocumentEvent result = null; |
| try { |
| int dirtyStartPos = -1; |
| String proposedDeletion = fStructuredDocument.get(fStart, fLengthToReplace); |
| if (fStart < fStructuredDocument.getLength()) { |
| if ((fChanges.indexOf(singleQuote) > -1) || (proposedDeletion.indexOf(singleQuote) > -1)) { |
| IRegion singleQuoteRegion = getFindReplaceDocumentAdapter().find(fStart, singleQuote, false, false, false, false); |
| if (singleQuoteRegion != null) { |
| dirtyStartPos = singleQuoteRegion.getOffset(); |
| } |
| } else if ((fChanges.indexOf(doubleQuote) > -1) || (proposedDeletion.indexOf(doubleQuote) > -1)) { |
| IRegion doubleQuoteRegion = getFindReplaceDocumentAdapter().find(fStart, doubleQuote, false, false, false, false); |
| if (doubleQuoteRegion != null) { |
| dirtyStartPos = doubleQuoteRegion.getOffset(); |
| } |
| } |
| } |
| if (dirtyStartPos > -1) { |
| // then we found one, do create new structuredDocument event |
| // based on the previous quote to end of document |
| // except, we need to be positive that the previous quote is |
| // in a "safe start" region (e.g. if in JSP content, we need |
| // to |
| // backup till we include the whole JSP region, in order for |
| // it |
| // to be correctly re-parsed. The backing up is done in the |
| // reparse/find dirty start from hint |
| // method. |
| result = reparse(dirtyStartPos, fStructuredDocument.getLength() - 1); |
| } |
| } catch (BadLocationException e) { |
| Logger.logException(e); |
| } |
| if (result != null) { |
| result.setDeletedText(fDeletedText); |
| } |
| return result; |
| } |
| |
| private StructuredDocumentEvent checkHeuristics() { |
| StructuredDocumentEvent result = null; |
| result = checkForNoChange(); |
| if (result == null) { |
| result = checkForCrossStructuredDocumentRegionBoundryCases(); |
| if (result == null) { |
| result = quickCheck(); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Takes into account "tag name" rules for comparisons; case-insensitive. |
| */ |
| private boolean checkTagNames(String compareText, String criticalTarget, boolean checkEnd) { |
| boolean result = false; |
| if ((compareText == null) || (criticalTarget == null)) |
| return false; |
| int posOfCriticalWord = compareText.toLowerCase().indexOf(criticalTarget.toLowerCase()); |
| result = posOfCriticalWord > -1; |
| if (checkEnd && result) { |
| // instead of returning true right away, we'll only return true |
| // the |
| // potentially matched tag is indeed a tag, for example, if |
| // <SCRIPT |
| // becomes <SCRIPTS we don't want to say the latter is a critical |
| // tag |
| int lastPos = posOfCriticalWord + criticalTarget.length(); |
| if (lastPos < compareText.length()) { |
| char lastChar = compareText.charAt(lastPos); |
| // Future: check formal definition of this java method, vs. |
| // XML |
| // parsing rules |
| result = (!Character.isLetterOrDigit(lastChar)); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * The core reparsing method ... after the dirty start and dirty end have |
| * been calculated elsewhere, and the text updated. |
| */ |
| protected StructuredDocumentEvent core_reparse(int rescanStart, int rescanEnd, CoreNodeList oldNodes, boolean firstTime) { |
| IStructuredDocumentRegion newNodesHead = null; |
| StructuredDocumentEvent result = null; |
| newNodesHead = _core_reparse_text(rescanStart, rescanEnd); |
| result = _core_reparse_update_model(newNodesHead, rescanStart, rescanEnd, oldNodes, firstTime); |
| return result; |
| } |
| |
| /** |
| * Resets state to "not parsing" |
| */ |
| private synchronized void endReParse() { |
| isParsing = false; |
| dirtyStart = null; |
| dirtyEnd = null; |
| } |
| |
| protected IStructuredDocumentRegion findDirtyEnd(int end) { |
| // Caution: here's one place we have to cast |
| IStructuredDocumentRegion result = fStructuredDocument.getRegionAtCharacterOffset(end); |
| // if not well formed, get one past, if there is something there |
| if ((result != null) && (!result.isEnded())) { |
| if (result.getNext() != null) { |
| result = result.getNext(); |
| } |
| } |
| // also, get one past if exactly equal to the end (this was needed |
| // as a simple fix to when a whole exact region is deleted. |
| // there's probably a better way. |
| if ((result != null) && (end == result.getEnd())) { |
| if (result.getNext() != null) { |
| result = result.getNext(); |
| } |
| } |
| // moved to subclass for quick transition |
| // 12/6/2001 - Since we've changed the parser/scanner to allow a lone |
| // '<' without |
| // always interpretting it as start of a tag name, we need to be a |
| // little fancier, in order |
| // to "skip" over any plain 'ol content between the lone '<' and any |
| // potential meating |
| // regions past plain 'ol content. |
| // if (isLoneOpenFollowedByContent(result) && (result.getNext() != |
| // null)) { |
| // result = result.getNext(); |
| // } |
| if (result != null) |
| fStructuredDocument.setCachedDocumentRegion(result); |
| dirtyEnd = result; |
| return dirtyEnd; |
| } |
| |
| protected void findDirtyStart(int start) { |
| IStructuredDocumentRegion result = fStructuredDocument.getRegionAtCharacterOffset(start); |
| // heuristic: if the postion is exactly equal to the start, then |
| // go back one more, if it exists. This prevents problems with |
| // insertions |
| // of text that should be merged with the previous node instead of |
| // simply hung |
| // off of it as a separate node (ex.: XML content inserted right |
| // before |
| // an open |
| // bracket should become part of the previous content node) |
| if (result != null) { |
| IStructuredDocumentRegion previous = result.getPrevious(); |
| if ((previous != null) && ((!(previous.isEnded())) || (start == result.getStart()))) { |
| result = previous; |
| } |
| // If we are now at the end of a "tag dependent" content area (or |
| // JSP area) |
| // then we need to back up all the way to the beginning of that. |
| IStructuredDocumentRegion potential = result; |
| // moved to subclass to speed transition |
| // while (isPartOfBlockRegion(potential)) { |
| // potential = potential.getPrevious(); |
| // } |
| if (potential != null) { |
| result = potential; |
| fStructuredDocument.setCachedDocumentRegion(result); |
| } |
| } |
| dirtyStart = result; |
| } |
| |
| protected CoreNodeList formOldNodes(IStructuredDocumentRegion dirtyStart, IStructuredDocumentRegion dirtyEnd) { |
| CoreNodeList oldNodes = new CoreNodeList(dirtyStart, dirtyEnd); |
| // Now save the old text, that "goes with" the old nodes and regions. |
| // Notice we are getting it directly from the text store |
| String oldText = null; |
| int oldStart = -1; |
| int oldEnd = -1; |
| // make sure there is some text, if not, use empty string |
| // (if one node is not null, the other should ALWAYS be not null too, |
| // since it |
| // would at least be equal to it.) |
| if (dirtyStart != null) { |
| oldStart = dirtyStart.getStart(); |
| oldEnd = dirtyEnd.getEnd(); |
| oldText = fStructuredDocument.get(oldStart, oldEnd - oldStart); |
| } else { |
| oldStart = 0; |
| oldEnd = 0; |
| oldText = ""; //$NON-NLS-1$ |
| } |
| // create a temporary text store for this text |
| SubSetTextStore subTextStore = new SubSetTextStore(oldText, oldStart, oldEnd, fStructuredDocument.getLength()); |
| // Now update the text store of the oldNodes |
| StructuredDocumentRegionIterator.setParentDocument(oldNodes, new MinimalDocument(subTextStore)); |
| return oldNodes; |
| } |
| |
| /** |
| * @return Returns the findReplaceDocumentAdapter. |
| */ |
| public FindReplaceDocumentAdapter getFindReplaceDocumentAdapter() { |
| if (fFindReplaceDocumentAdapter == null) { |
| fFindReplaceDocumentAdapter = new FindReplaceDocumentAdapter(fStructuredDocument); |
| } |
| return fFindReplaceDocumentAdapter; |
| } |
| |
| // Note: if thead safety is needed, this and all the other public methods |
| // of this class |
| // should be synchronized. |
| public void initialize(Object requester, int start, int lengthToReplace, String changes) { |
| isParsing = true; |
| fRequester = requester; |
| fStart = start; |
| fLengthToReplace = lengthToReplace; |
| fChanges = changes; |
| // notice this one is derived |
| fLengthDifference = Utilities.calculateLengthDifference(fChanges, fLengthToReplace); |
| fDeletedText = fStructuredDocument.get(fStart, fLengthToReplace); |
| } |
| |
| protected void insertNodes(IStructuredDocumentRegion previousOldNode, IStructuredDocumentRegion nextOldNode, CoreNodeList newNodes) { |
| // |
| IStructuredDocumentRegion firstNew = null; |
| IStructuredDocumentRegion lastNew = null; |
| // |
| IStructuredDocumentRegion oldPrevious = previousOldNode; |
| IStructuredDocumentRegion oldNext = nextOldNode; |
| // |
| if (newNodes.getLength() > 0) { |
| // get pointers |
| firstNew = newNodes.item(0); |
| lastNew = newNodes.item(newNodes.getLength() - 1); |
| // switch surrounding StructuredDocumentRegions' references to |
| // lists |
| if (oldPrevious != null) |
| oldPrevious.setNext(firstNew); |
| if (oldNext != null) { |
| oldNext.setPrevious(lastNew); |
| } else { |
| // SIDE EFFECT |
| // if oldNext is null, that means we are replaceing the |
| // lastNode in the chain, |
| // so we need to update the structuredDocuments lastNode as |
| // the |
| // last of the new nodes. |
| fStructuredDocument.setLastDocumentRegion(newNodes.item(newNodes.getLength() - 1)); |
| } |
| if (firstNew != null) |
| firstNew.setPrevious(oldPrevious); |
| if (lastNew != null) |
| lastNew.setNext(oldNext); |
| } |
| // else nothing to insert |
| } |
| |
| /** |
| * @param oldRegion |
| */ |
| private boolean isCollectionRegion(ITextRegion aRegion) { |
| return (aRegion instanceof ITextRegionCollection); |
| } |
| |
| /** |
| * @return boolean |
| */ |
| public boolean isParsing() { |
| return isParsing; |
| } |
| |
| /** |
| * The minimization algorithm simply checks the old nodes to see if any of |
| * them "survived" the rescan and are unchanged. If so, the instance of |
| * the old node is used instead of the new node. Before the requested |
| * change, need to check type, offsets, and text to determine if the same. |
| * After the requested change, need to check type and text, but adjust the |
| * offsets to what ever the change was. |
| */ |
| protected StructuredDocumentEvent minimumEvent(CoreNodeList oldNodes, CoreNodeList newNodes) { |
| StructuredDocumentEvent event = null; |
| CoreNodeList minimalOldNodes = null; |
| CoreNodeList minimalNewNodes = null; |
| // To minimize nodes, we'll collect all those |
| // that are not equal into old and new lists |
| // Note: we assume that old and new nodes |
| // are basically contiguous -- and we force it to be so, |
| // by starting at the beginning to |
| // find first difference, and then starting at the end to find |
| // last difference. Everything in between we assume is different. |
| // |
| // |
| // |
| // startOfDifferences is the index into the core node list where the |
| // first difference |
| // occurs. But it may point into the old or the new list. |
| int startOfDifferences = _computeStartOfDifferences(oldNodes, newNodes); |
| int endOfDifferencesOld = -1; |
| int endOfDifferencesNew = -1; |
| // if one of the lists are shorter than where the differences start, |
| // then |
| // then some portion of the lists are identical |
| if ((startOfDifferences >= oldNodes.getLength()) || (startOfDifferences >= newNodes.getLength())) { |
| if (oldNodes.getLength() < newNodes.getLength()) { |
| // Then there are new regions to add |
| // these lengths will cause the vector of old ones to not |
| // have any elements, and the vector of new regions to have |
| // just the new ones not in common with the old ones |
| //startOfDifferences should equal oldNodes.getLength(), |
| // calculated above on _computeStartOfDifferences |
| minimalOldNodes = EMPTY_LIST; |
| endOfDifferencesNew = newNodes.getLength() - 1; |
| minimalNewNodes = _formMinimumList(newNodes, startOfDifferences, endOfDifferencesNew); |
| } else { |
| if (oldNodes.getLength() > newNodes.getLength()) { |
| // delete old |
| // then there are old regions to delete |
| // these lengths will cause the vector of old regions to |
| // contain the ones to delete, and the vector of new |
| // regions |
| // not have any elements |
| //startOfDifferences should equal newNodes.getLength(), |
| // calculated above on _computeStartOfDifferences |
| endOfDifferencesOld = oldNodes.getLength() - 1; |
| minimalOldNodes = _formMinimumList(oldNodes, startOfDifferences, endOfDifferencesOld); |
| minimalNewNodes = EMPTY_LIST; |
| } else |
| // unlikely event |
| event = new NoChangeEvent(fStructuredDocument, fRequester, fChanges, fStart, fLengthToReplace); |
| } |
| } else { |
| // We found a normal startOfDiffernces, but have not yet found the |
| // ends. |
| // We'll look for the end of differences by going backwards down |
| // the two lists. |
| // Here we need a seperate index for each array, since they may be |
| // (and |
| // probably are) of different lengths. |
| int indexOld = oldNodes.getLength() - 1; |
| int indexNew = newNodes.getLength() - 1; |
| // The greaterThanEffectedRegion is important to gaurd against |
| // incorrect counting |
| // when something identical is inserted to what's already there |
| // (see minimization test case 5) |
| // Note: the indexOld > startOfDifferences keeps indexOld from |
| // getting too small, |
| // so that the subsequent oldNodes.item(indexOld) is always valid. |
| while ((indexOld >= startOfDifferences) && (_greaterThanEffectedRegion(oldNodes.item(indexOld)))) { |
| if (!(oldNodes.item(indexOld).sameAs(newNodes.item(indexNew), fLengthDifference))) { |
| break; |
| } else { |
| // if they are equal, then we will be keeping the old one, |
| // so |
| // we need to be sure its parentDocument is set back to |
| // the |
| // right instance |
| oldNodes.item(indexOld).setParentDocument(fStructuredDocument); |
| } |
| indexOld--; |
| indexNew--; |
| } |
| endOfDifferencesOld = indexOld; |
| endOfDifferencesNew = indexNew; |
| minimalOldNodes = _formMinimumList(oldNodes, startOfDifferences, endOfDifferencesOld); |
| minimalNewNodes = _formMinimumList(newNodes, startOfDifferences, endOfDifferencesNew); |
| } ///////////////////////////////////////// |
| // |
| IStructuredDocumentRegion firstDownStreamNode = null; |
| event = regionCheck(minimalOldNodes, minimalNewNodes); |
| if (event != null) { |
| firstDownStreamNode = minimalOldNodes.item(0).getNext(); |
| if (firstDownStreamNode != null && fLengthDifference != 0) { // if |
| // firstDownStream |
| // is |
| // null, |
| // then |
| // we're |
| // at |
| // the |
| // end |
| // of |
| // the |
| // document |
| StructuredDocumentRegionIterator.adjustStart(firstDownStreamNode, fLengthDifference); |
| } // |
| } else { |
| event = nodesReplacedCheck(minimalOldNodes, minimalNewNodes); |
| // now splice the new chain of nodes to where the old chain is (or |
| // was) |
| // the firstDownStreamNode (the first of those after the new |
| // nodes) |
| // is |
| // remembered as a tiny optimization. |
| if (minimalOldNodes.getLength() == 0 && minimalNewNodes.getLength() > 0) { |
| // if no old nodes are being deleted, then use the |
| // the newNodes offset (minus one) to find the point to |
| // update downstream nodes, and after updating downstream |
| // nodes postions, insert the new ones. |
| int insertOffset = minimalNewNodes.item(0).getStartOffset(); |
| IStructuredDocumentRegion lastOldUnchangedNode = null; |
| if (insertOffset > 0) { |
| lastOldUnchangedNode = fStructuredDocument.getRegionAtCharacterOffset(insertOffset - 1); |
| firstDownStreamNode = lastOldUnchangedNode.getNext(); |
| } else { |
| // we're inserting at very beginning |
| firstDownStreamNode = fStructuredDocument.getFirstStructuredDocumentRegion(); |
| // SIDE EFFECT: change the firstNode pointer if we're |
| // inserting at beginning |
| fStructuredDocument.setFirstDocumentRegion(minimalNewNodes.item(0)); |
| } |
| StructuredDocumentRegionIterator.adjustStart(firstDownStreamNode, fLengthDifference); |
| insertNodes(lastOldUnchangedNode, firstDownStreamNode, minimalNewNodes); |
| // this (nodes replaced) is the only case where we need to |
| // update the cached Node |
| reSetCachedNode(minimalOldNodes, minimalNewNodes); |
| } else { |
| firstDownStreamNode = switchNodeLists(minimalOldNodes, minimalNewNodes); |
| // no need to adjust the length of the new nodes themselves, |
| // they |
| // are already correct, but we do need to |
| // adjust all "down stream" nodes with the length of the |
| // insertion or deletion |
| // --- adjustment moved to calling method. |
| if (firstDownStreamNode != null) { |
| // && event != null |
| StructuredDocumentRegionIterator.adjustStart(firstDownStreamNode, fLengthDifference); |
| } // |
| // this (nodes replaced) is the only case where we need to |
| // update the cached Node |
| reSetCachedNode(minimalOldNodes, minimalNewNodes); |
| } |
| } |
| return event; |
| } |
| |
| // TODO: This should be abstract. |
| public IStructuredTextReParser newInstance() { |
| return new StructuredDocumentReParser(); |
| } |
| |
| protected StructuredDocumentEvent nodesReplacedCheck(CoreNodeList oldNodes, CoreNodeList newNodes) { |
| // actually, nothing to check here, since (and assuming) we've already |
| // minimized the number of nodes, and ruled out mere region changes |
| StructuredDocumentEvent result = new StructuredDocumentRegionsReplacedEvent(fStructuredDocument, fRequester, oldNodes, newNodes, fChanges, fStart, fLengthToReplace); |
| return result; |
| } |
| |
| /** |
| * A method to allow any heuristic "quick checks" that might cover many |
| * many cases, before expending the time on a full reparse. |
| * |
| */ |
| public StructuredDocumentEvent quickCheck() { |
| StructuredDocumentEvent result = null; |
| // if the dirty start is null, then we have an empty document. |
| // in which case we'll return null so everything can be |
| // reparsed "from scratch" . If its not null, we'll give the flatnode |
| // a |
| // chance |
| // to handle, but only if there is one flatnode involved. |
| if (dirtyStart != null && dirtyStart == dirtyEnd) { |
| IStructuredDocumentRegion targetNode = dirtyStart; |
| result = dirtyStart.updateRegion(fRequester, targetNode, fChanges, fStart, fLengthToReplace); |
| if (result != null) { |
| // at this point only, we need to update the text store and |
| // and downstream nodes. |
| // FUTURE_TO_DO: can this dependency on structuredDocument |
| // method be eliminated? |
| fStructuredDocument.updateDocumentData(fStart, fLengthToReplace, fChanges); |
| IStructuredDocumentRegion firstDownStreamNode = targetNode.getNext(); |
| // then flatnode must have been the last one, so need to |
| // update |
| // any downstream ones |
| if (firstDownStreamNode != null) { |
| StructuredDocumentRegionIterator.adjustStart(firstDownStreamNode, fLengthDifference); |
| } |
| } |
| } |
| if (result != null) { |
| result.setDeletedText(fDeletedText); |
| } |
| return result; |
| } |
| |
| /** |
| * If only one node is involved, sees how many regions are changed. If |
| * only one, then its a 'regionChanged' event ... if more than one, its a |
| * 'regionsReplaced' event. |
| */ |
| protected StructuredDocumentEvent regionCheck(CoreNodeList oldNodes, CoreNodeList newNodes) { |
| if (Debug.debugStructuredDocument) |
| System.out.println("IStructuredDocument::regionsReplacedCheck"); //$NON-NLS-1$ |
| //$NON-NLS-1$ |
| //$NON-NLS-1$ |
| // the "regionsReplaced" event could only be true if and only if the |
| // nodelists |
| // are each only "1" in length. |
| StructuredDocumentEvent result = null; |
| int oldLength = oldNodes.getLength(); |
| int newLength = newNodes.getLength(); |
| if ((oldLength != 1) || (newLength != 1)) { |
| result = null; |
| } else { |
| IStructuredDocumentRegion oldNode = oldNodes.item(0); |
| IStructuredDocumentRegion newNode = newNodes.item(0); |
| result = regionCheck(oldNode, newNode); |
| } |
| return result; |
| } |
| |
| /** |
| * If only one node is involved, sees how many regions are changed. If |
| * only one, then its a 'regionChanged' event ... if more than one, its a |
| * 'regionsReplaced' event. |
| */ |
| protected StructuredDocumentEvent regionCheck(IStructuredDocumentRegion oldNode, IStructuredDocumentRegion newNode) { |
| // |
| StructuredDocumentEvent result = null; |
| ITextRegionList oldRegions = oldNode.getRegions(); |
| ITextRegionList newRegions = newNode.getRegions(); |
| ITextRegion[] oldRegionsArray = oldRegions.toArray(); |
| ITextRegion[] newRegionsArray = newRegions.toArray(); |
| // |
| // for the 'regionsReplaced' event, we don't care if |
| // the regions changed due to type, or text, |
| // we'll just collect all those that are not equal |
| // into the old and new region lists. |
| // Note: we, of course, assume that old and new regions |
| // are basically contiguous -- and we force it to be so, |
| // even if not literally so, by starting at beginning to |
| // find first difference, and then starting at end to find |
| // last difference. Everything in between we assume is different. |
| // |
| // going up is easy, we start at zero in each, and continue |
| // till regions are not the same. |
| int startOfDifferences = _computeStartOfDifferences(oldNode, oldRegions, newNode, newRegions); |
| int endOfDifferencesOld = -1; |
| int endOfDifferencesNew = -1; |
| // |
| // |
| // if one of the lists are shorter than where the differences start, |
| // then |
| // then some portion of the lists are identical |
| if ((startOfDifferences >= oldRegions.size()) || (startOfDifferences >= newRegions.size())) { |
| if (oldRegions.size() < newRegions.size()) { |
| // INSERT CASE |
| // then there are new regions to add |
| // these lengths will cause the vector of old ones to not |
| // have any elements, and the vector of new regions to have |
| // just the new ones. |
| startOfDifferences = oldRegionsArray.length; |
| endOfDifferencesOld = oldRegionsArray.length - 1; |
| endOfDifferencesNew = newRegionsArray.length - 1; |
| } else { |
| if (oldRegions.size() > newRegions.size()) { |
| // DELETE CASE |
| // then there are old regions to delete |
| // these lengths will cause the vector of old regions to |
| // contain the ones to delete, and the vector of new |
| // regions |
| // not have any elements |
| startOfDifferences = newRegionsArray.length; |
| endOfDifferencesOld = oldRegionsArray.length - 1; |
| endOfDifferencesNew = newRegionsArray.length - 1; |
| } else { |
| // else the lists are identical! |
| // unlikely event, probably error in current design, since |
| // we check for identity at the very beginning of |
| // reparsing. |
| result = new NoChangeEvent(fStructuredDocument, fRequester, fChanges, fStart, fLengthToReplace); |
| } |
| } |
| } else { |
| if ((startOfDifferences > -1) && (endOfDifferencesOld < 0) && (endOfDifferencesNew < 0)) { |
| // We found a normal startOfDiffernces, but have not yet found |
| // the ends. |
| // We'll look for the end of differences by going backwards |
| // down the two lists. |
| // Here we need a seperate index for each array, since they |
| // may |
| // be (and |
| // probably are) of different lengths. |
| int indexOld = oldRegionsArray.length - 1; |
| int indexNew = newRegionsArray.length - 1; |
| while ((indexOld >= startOfDifferences) && (_greaterThanEffectedRegion(oldNode, oldRegionsArray[indexOld]))) { |
| if ((!(oldNode.sameAs(oldRegionsArray[indexOld], newNode, newRegionsArray[indexNew], fLengthDifference)))) { |
| //endOfDifferencesOld = indexOne; |
| //endOfDifferencesNew = indexTwo; |
| break; |
| } |
| indexOld--; |
| indexNew--; |
| } |
| endOfDifferencesOld = indexOld; |
| endOfDifferencesNew = indexNew; |
| } |
| } |
| // |
| // result != null means the impossible case above occurred |
| if (result == null) { |
| // Now form the two vectors of different regions |
| ITextRegionList holdOldRegions = new TextRegionListImpl(); |
| ITextRegionList holdNewRegions = new TextRegionListImpl(); |
| if (startOfDifferences > -1 && endOfDifferencesOld > -1) { |
| for (int i = startOfDifferences; i <= endOfDifferencesOld; i++) { |
| holdOldRegions.add(oldRegionsArray[i]); |
| } |
| } |
| if (startOfDifferences > -1 && endOfDifferencesNew > -1) { |
| for (int i = startOfDifferences; i <= endOfDifferencesNew; i++) { |
| holdNewRegions.add(newRegionsArray[i]); |
| } |
| } |
| if (holdOldRegions.size() == 0 && holdNewRegions.size() == 0) { |
| // then this means the regions were identical, which means |
| // someone |
| // pasted exactly the same thing they had selected, or !!! |
| // someone deleted the end bracket of the tag. !!!? |
| result = new NoChangeEvent(fStructuredDocument, fRequester, fChanges, fStart, fLengthToReplace); |
| } else { |
| //If both holdOldRegions and holdNewRegions are of length 1, |
| // then its |
| // a "region changed" event, else a "regions replaced" event. |
| // so we want the new instance of region to become part of the |
| // old instance of old node |
| if ((holdOldRegions.size() == 1) && (holdNewRegions.size() == 1) && _regionsSameKind((holdNewRegions.get(0)), (holdOldRegions.get(0)))) { |
| ITextRegion newOldRegion = swapNewForOldRegion(oldNode, holdOldRegions.get(0), newNode, holdNewRegions.get(0)); |
| // -- need to update any down stream regions, within this |
| // 'oldNode' |
| updateDownStreamRegions(oldNode, newOldRegion); |
| result = new RegionChangedEvent(fStructuredDocument, fRequester, oldNode, newOldRegion, fChanges, fStart, fLengthToReplace); |
| } else { |
| replaceRegions(oldNode, holdOldRegions, newNode, holdNewRegions); |
| // -- need to update any down stream regions, within this |
| // 'oldNode' |
| // don't need with the way replaceRegions is implemented. |
| // It handles. |
| //if(holdNewRegions.size() > 0) |
| //updateDownStreamRegions(oldNode, (ITextRegion) |
| // holdNewRegions.lastElement()); |
| result = new RegionsReplacedEvent(fStructuredDocument, fRequester, oldNode, holdOldRegions, holdNewRegions, fChanges, fStart, fLengthToReplace); |
| } |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * An entry point for reparsing. It calculates the dirty start and dirty |
| * end flatnodes based on the start point and length of the changes. |
| * |
| */ |
| public StructuredDocumentEvent reparse() { |
| StructuredDocumentEvent result = null; |
| // if we do not have a cachedNode, then the document |
| // must be empty, so simply use 'null' as the dirtyStart and dirtyEnd |
| // otherwise, find them. |
| if (fStructuredDocument.getCachedDocumentRegion() != null) { |
| findDirtyStart(fStart); |
| int end = fStart + fLengthToReplace; |
| findDirtyEnd(end); |
| } |
| if (fStructuredDocument.getCachedDocumentRegion() != null) { |
| result = checkHeuristics(); |
| } |
| if (result == null) { |
| result = reparse(dirtyStart, dirtyEnd); |
| } |
| endReParse(); |
| return result; |
| } |
| |
| /** |
| * An entry point for reparsing. It calculates the dirty start and dirty |
| * end flatnodes based on suggested positions to begin and end. This is |
| * needed for cases where parsing must go beyond the immediate node and |
| * its direct neighbors. |
| * |
| */ |
| protected StructuredDocumentEvent reparse(int reScanStartHint, int reScanEndHint) { |
| StructuredDocumentEvent result = null; |
| // if we do not have a cachedNode, then the document |
| // must be empty, so simply use 'null' as the dirtyStart and dirtyEnd |
| if (fStructuredDocument.getCachedDocumentRegion() != null) { |
| findDirtyStart(reScanStartHint); |
| findDirtyEnd(reScanEndHint); |
| } |
| result = reparse(dirtyStart, dirtyEnd); |
| isParsing = false; |
| // debug |
| //verifyStructured(result); |
| return result; |
| } |
| |
| /** |
| * The core reparsing method ... after the dirty start and dirty end have |
| * been calculated elsewhere. |
| */ |
| protected StructuredDocumentEvent reparse(IStructuredDocumentRegion dirtyStart, IStructuredDocumentRegion dirtyEnd) { |
| StructuredDocumentEvent result = null; |
| int rescanStart = -1; |
| int rescanEnd = -1; |
| boolean firstTime = false; |
| // |
| // "save" the oldNodes (that may be replaced) in a list |
| CoreNodeList oldNodes = formOldNodes(dirtyStart, dirtyEnd); |
| if (dirtyStart == null || dirtyEnd == null) { |
| // dirtyStart or dirty end are null, then that means we didn't |
| // have |
| // a |
| // cached node, which means we have an empty document, so we |
| // just need to rescan the changes |
| rescanStart = 0; |
| rescanEnd = fChanges.length(); |
| firstTime = true; |
| } else { |
| // set the start of the text to rescan |
| rescanStart = dirtyStart.getStart(); |
| // |
| // set the end of the text to rescan |
| // notice we use the same rationale as for the rescanStart, |
| // with the added caveat that length has to be added to it, |
| // to compensate for the new text which has been added or deleted. |
| // If changes has zero length, then "length" will be negative, |
| // since |
| // we are deleting text. Otherwise, use the difference between |
| // what's selected to be replaced and the length of the new text. |
| rescanEnd = dirtyEnd.getEnd() + fLengthDifference; |
| } |
| // now that we have the old stuff "saved" away, update the document |
| // with the changes. |
| // FUTURE_TO_DO -- don't fire "document changed" event till later |
| fStructuredDocument.updateDocumentData(fStart, fLengthToReplace, fChanges); |
| // ------------------ now the real work |
| result = core_reparse(rescanStart, rescanEnd, oldNodes, firstTime); |
| // |
| // event is returned to the caller, incase there is |
| // some opitmization they can do |
| return result; |
| } |
| |
| protected void replaceRegions(IStructuredDocumentRegion oldNode, ITextRegionList oldRegions, IStructuredDocumentRegion newNode, ITextRegionList newRegions) { |
| int insertPos = -1; |
| ITextRegionList regions = oldNode.getRegions(); |
| // make a fake flatnode to be new parent of oldRegions, so their text |
| // will be right. |
| //IStructuredDocumentRegion holdOldStructuredDocumentRegion = new |
| // BasicStructuredDocumentRegion(oldNode); |
| // |
| // need to reset the parent of the new to-be-inserted regions to be |
| // the |
| // same oldNode that is the one having its regions changed |
| // DW, 4/16/2003, removed since ITextRegion no longer has parent. |
| // ITextRegionContainer oldParent = oldNode; |
| // for (int i = 0; i < newRegions.size(); i++) { |
| // AbstractRegion region = (AbstractRegion) newRegions.elementAt(i); |
| // region.setParent(oldParent); |
| // } |
| // if there are no old regions, insert the new regions according to |
| // offset |
| if (oldRegions.size() == 0) { |
| ITextRegion firstNewRegion = newRegions.get(0); |
| int firstOffset = newNode.getStartOffset(firstNewRegion); |
| // if at beginning, insert there |
| if (firstOffset == 0) { |
| insertPos = 0; |
| } else { |
| // |
| ITextRegion regionAtOffset = oldNode.getRegionAtCharacterOffset(firstOffset); |
| if (regionAtOffset == null) |
| insertPos = regions.size(); |
| else |
| insertPos = regions.indexOf(regionAtOffset); |
| } |
| } else { |
| // else, delete old ones before inserting new ones in their place |
| ITextRegion firstOldRegion = oldRegions.get(0); |
| insertPos = regions.indexOf(firstOldRegion); |
| regions.removeAll(oldRegions); |
| } |
| regions.addAll(insertPos, newRegions); |
| // now regions vector of each node should be of equal length, |
| // so go through each, and make sure the old regions |
| // offsets matches the new regions offsets |
| // (we'll just assign them all, but could be slightly more effiecient) |
| ITextRegionList allNewRegions = newNode.getRegions(); |
| for (int i = 0; i < regions.size(); i++) { |
| ITextRegion nextOldishRegion = regions.get(i); |
| ITextRegion nextNewRegion = allNewRegions.get(i); |
| nextOldishRegion.equatePositions(nextNewRegion); |
| checkAndAssignParent(oldNode, nextOldishRegion); |
| } |
| oldNode.setLength(newNode.getLength()); |
| oldNode.setEnded(newNode.isEnded()); |
| oldNode.setParentDocument(newNode.getParentDocument()); |
| // removed concept of part of these regions, so no longer need to do. |
| // for (int i = 0; i < oldRegions.size(); i++) { |
| // ITextRegion region = (ITextRegion) oldRegions.elementAt(i); |
| // region.setParent(holdOldStructuredDocumentRegion); |
| // } |
| } |
| |
| private void reSetCachedNode(CoreNodeList oldNodes, CoreNodeList newNodes) { |
| // use the last newNode as the new cachedNode postion, unless its null |
| // (e.g. when nodes are deleted) in which case, assign |
| // it to a "safe" node so we don't lose reference to the |
| // structuredDocument! |
| if (newNodes.getLength() > 0) { |
| // use last new node as the cache |
| fStructuredDocument.setCachedDocumentRegion(newNodes.item(newNodes.getLength() - 1)); |
| } else { |
| // if cachedNode is an old node, then we're in trouble: |
| // we can't leave it as the cached node! and its already |
| // been disconnected from the model, so we can't do getNext |
| // or getPrevious, so we'll get one that is right before |
| // (or right after) the offset of the old nodes that are being |
| // deleted. |
| // |
| // if newNodesHead and cachedNode are both null, then |
| // it means we were asked to insert an empty string into |
| // an empty document. So we have nothing to do here |
| // (that is, we have no node to cache) |
| // similarly if there are no new nodes and no old nodes then |
| // nothing to do (but that should never happen ... we shouldn't |
| // get there if there is no event to generate). |
| if ((fStructuredDocument.getCachedDocumentRegion() != null) && (oldNodes.getLength() > 0)) { |
| // note: we can't simple use nodeAtCharacterOffset, since it |
| // depends on cachedNode. |
| if (oldNodes.includes(fStructuredDocument.getCachedDocumentRegion())) |
| fStructuredDocument.setCachedDocumentRegion(fStructuredDocument.getFirstStructuredDocumentRegion()); |
| } |
| if ((fStructuredDocument.getCachedDocumentRegion() == null) && (Debug.displayWarnings)) { |
| // this will happen now legitamately when all text is deleted |
| // from a document |
| System.out.println("Warning: StructuredDocumentReParser::reSetCachedNode: could not find a node to cache! (its ok if all text deleted)"); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| public void setStructuredDocument(IStructuredDocument newStructuredDocument) { |
| // NOTE: this method (and class) depend on being able to |
| // do the following cast (i.e. references some fields directly) |
| fStructuredDocument = (BasicStructuredDocument) newStructuredDocument; |
| fFindReplaceDocumentAdapter = null; |
| } |
| |
| private IStructuredDocumentRegion splice(CoreNodeList oldNodes, CoreNodeList newNodes) { |
| // |
| IStructuredDocumentRegion firstOld = null; |
| IStructuredDocumentRegion firstNew = null; |
| IStructuredDocumentRegion lastOld = null; |
| IStructuredDocumentRegion lastNew = null; |
| // |
| IStructuredDocumentRegion oldPrevious = null; |
| IStructuredDocumentRegion oldNext = null; |
| IStructuredDocumentRegion newPrevious = null; |
| IStructuredDocumentRegion newNext = null; |
| // |
| // if called with both arguments empty lists, we can disregard. |
| // this happens, for example, when some text is replaced with the |
| // identical text. |
| if ((oldNodes.getLength() == 0) && (newNodes.getLength() == 0)) { |
| return null; |
| } |
| // get pointers |
| if (newNodes.getLength() > 0) { |
| firstNew = newNodes.item(0); |
| lastNew = newNodes.item(newNodes.getLength() - 1); |
| } |
| // |
| if (oldNodes.getLength() > 0) { |
| firstOld = oldNodes.item(0); |
| lastOld = oldNodes.item(oldNodes.getLength() - 1); |
| if (firstOld != null) |
| oldPrevious = firstOld.getPrevious(); |
| if (lastOld != null) |
| oldNext = lastOld.getNext(); |
| } |
| // handle switch |
| if (newNodes.getLength() > 0) { |
| // switch surrounding StructuredDocumentRegions' references to |
| // lists |
| if (oldPrevious != null) |
| oldPrevious.setNext(firstNew); |
| if (newPrevious != null) |
| newPrevious.setNext(firstOld); |
| if (oldNext != null) |
| oldNext.setPrevious(lastNew); |
| if (newNext != null) |
| newNext.setPrevious(lastOld); |
| // switch list pointers to surrounding StructuredDocumentRegions |
| if (firstOld != null) |
| firstOld.setPrevious(newPrevious); |
| if (lastOld != null) |
| lastOld.setNext(newNext); |
| if (firstNew != null) |
| firstNew.setPrevious(oldPrevious); |
| if (lastNew != null) |
| lastNew.setNext(oldNext); |
| } else { |
| // short circuit when there are no new nodes |
| if (oldPrevious != null) |
| oldPrevious.setNext(oldNext); |
| if (oldNext != null) |
| oldNext.setPrevious(oldPrevious); |
| } |
| // |
| // SIDE EFFECTs |
| // if we have oldNodes, and if oldNext or oldPrevious is null, |
| // that means we are replacing |
| // the lastNode or firstNode the structuredDocuments's chain of nodes, |
| // so we need to update the structuredDocuments last or first Node |
| // as the last or first of the new nodes. |
| // (and sometimes even these will be null! such as when deleting all |
| // text in a document). |
| if ((oldNext == null) && (oldNodes.getLength() > 0)) { |
| if (newNodes.getLength() > 0) { |
| fStructuredDocument.setLastDocumentRegion(lastNew); |
| } else { |
| // in this case, the last node is being deleted, but not |
| // replaced |
| // with anything. In this case, we can just back up one |
| // from the first old node |
| fStructuredDocument.setLastDocumentRegion(firstOld.getPrevious()); |
| } |
| } |
| if ((oldPrevious == null) && (oldNodes.getLength() > 0)) { |
| if (newNodes.getLength() > 0) { |
| fStructuredDocument.setFirstDocumentRegion(firstNew); |
| } else { |
| // in this case the first node is being deleted, but not |
| // replaced |
| // with anything. So, we just go one forward past the last old |
| // node. |
| fStructuredDocument.setFirstDocumentRegion(lastOld.getNext()); |
| } |
| } |
| // as a tiny optimization, we return the first of the downstream |
| // nodes, |
| // if any |
| return oldNext; |
| } |
| |
| /** |
| * The purpose of this method is to "reuse" the old container region, when |
| * found to be same (so same instance doesn't change). The goal is to |
| * "transform" the old region, so its equivelent to the newly parsed one. |
| * |
| */ |
| private ITextRegion swapNewForOldRegion(IStructuredDocumentRegion oldNode, ITextRegion oldRegion, IStructuredDocumentRegion newNode, ITextRegion newRegion) { |
| // makes the old region instance the correct size. |
| oldRegion.equatePositions(newRegion); |
| // adjusts old node instance appropriately |
| oldNode.setLength(newNode.getLength()); |
| oldNode.setEnded(newNode.isEnded()); |
| // we do have to set the parent document, since the oldNode's |
| // were set to a temporary one, then newNode's have the |
| // right one. |
| oldNode.setParentDocument(newNode.getParentDocument()); |
| // if we're transforming a container region, we need to be sure to |
| // transfer the new embedded regions, to the old parent |
| // Note: if oldRegion hasEmbeddedRegions, then we know the |
| // newRegion does too, since we got here because they were the |
| // same type. |
| if (isCollectionRegion(oldRegion)) { // || |
| // hasContainerRegions(oldRegion)) |
| // { |
| transferEmbeddedRegions(oldNode, (ITextRegionContainer) oldRegion, (ITextRegionContainer) newRegion); |
| } |
| return oldRegion; |
| } |
| |
| private IStructuredDocumentRegion switchNodeLists(CoreNodeList oldNodes, CoreNodeList newNodes) { |
| IStructuredDocumentRegion result = splice(oldNodes, newNodes); |
| // ensure that the old nodes hold no references to the existing model |
| if (oldNodes.getLength() > 0) { |
| IStructuredDocumentRegion firstItem = oldNodes.item(0); |
| firstItem.setPrevious(null); |
| IStructuredDocumentRegion lastItem = oldNodes.item(oldNodes.getLength() - 1); |
| lastItem.setNext(null); |
| } |
| return result; |
| } |
| |
| /** |
| * The purpose of this method is to "reuse" the old container region, when |
| * found to be same (so same instance doesn't change). The goal is to |
| * "transform" the old region, so its equivelent to the newly parsed one. |
| * |
| */ |
| private void transferEmbeddedRegions(IStructuredDocumentRegion oldNode, ITextRegionContainer oldRegion, ITextRegionContainer newRegion) { |
| // the oldRegion should already have the right parent, since |
| // we got here because all's equivelent except the region |
| // postions have changed. |
| //oldRegion.setParent(newRegion.getParent()); |
| // but we should check if there's "nested" embedded regions, and if |
| // so, we can just move them over. setting their parent as this old |
| // region. |
| ITextRegionList newRegionsToTransfer = newRegion.getRegions(); |
| oldRegion.setRegions(newRegionsToTransfer); |
| Iterator newRegionsInOldOne = newRegionsToTransfer.iterator(); |
| while (newRegionsInOldOne.hasNext()) { |
| ITextRegion newOne = (ITextRegion) newRegionsInOldOne.next(); |
| if (isCollectionRegion(newOne)) { // || |
| // hasContainerRegions(newOne)) { |
| //((ITextRegionContainer) newOne).setParent(oldRegion); |
| oldRegion.setRegions(newRegion.getRegions()); |
| } |
| } |
| } |
| |
| private void updateDownStreamRegions(IStructuredDocumentRegion flatNode, ITextRegion lastKnownRegion) { |
| // so all regions after the last known region (last known to be ok) |
| // have to have their start and end values adjusted. |
| ITextRegionList regions = flatNode.getRegions(); |
| int listLength = regions.size(); |
| int startIndex = 0; |
| // first, loop through to find where to start |
| for (int i = 0; i < listLength; i++) { |
| ITextRegion region = regions.get(i); |
| if (region == lastKnownRegion) { |
| startIndex = i; |
| break; |
| } |
| } |
| // now, beginning one past the last known one, loop |
| // through to end of list, adjusting the start and end postions. |
| startIndex++; |
| for (int j = startIndex; j < listLength; j++) { |
| ITextRegion region = regions.get(j); |
| region.adjustStart(fLengthDifference); |
| } |
| } |
| } |