| /**
|
| * Copyright (c) 2005-2008 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 - Initial API and implementation
|
| */
|
| package org.eclipse.egf.core.pde.internal.ui; |
| |
| import java.util.ArrayList; |
| |
| import org.eclipse.egf.common.constant.EGFCommonConstants; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.Region; |
| import org.eclipse.jface.text.TextUtilities; |
| import org.eclipse.pde.core.IModelChangedEvent; |
| import org.eclipse.pde.internal.core.text.AbstractTextChangeListener; |
| import org.eclipse.pde.internal.core.text.IDocumentAttributeNode; |
| import org.eclipse.pde.internal.core.text.IDocumentElementNode; |
| import org.eclipse.pde.internal.core.text.IDocumentTextNode; |
| import org.eclipse.pde.internal.core.util.PDEXMLHelper; |
| import org.eclipse.pde.internal.core.util.PropertiesUtil; |
| import org.eclipse.text.edits.DeleteEdit; |
| import org.eclipse.text.edits.InsertEdit; |
| import org.eclipse.text.edits.MoveSourceEdit; |
| import org.eclipse.text.edits.MoveTargetEdit; |
| import org.eclipse.text.edits.MultiTextEdit; |
| import org.eclipse.text.edits.ReplaceEdit; |
| import org.eclipse.text.edits.TextEdit; |
| |
| |
| /** |
| * Fix a PDE bug, https://bugs.eclipse.org/bugs/show_bug.cgi?id=184737 |
| */ |
| public class XMLTextChangeListener extends AbstractTextChangeListener { |
| |
| @SuppressWarnings("unchecked") |
| private ArrayList fOperationList = new ArrayList(); |
| |
| public XMLTextChangeListener(IDocument document) { |
| super(document); |
| } |
| |
| public TextEdit[] getTextOperations() { |
| if (fOperationList.size() == 0) |
| return new TextEdit[0]; |
| |
| MultiTextEdit edit = new MultiTextEdit(); |
| try { |
| if (PropertiesUtil.isNewlineNeeded(fDocument)) |
| insert(edit, new InsertEdit(fDocument.getLength(), TextUtilities.getDefaultLineDelimiter(fDocument))); |
| } catch (BadLocationException e) { |
| // do nothing. |
| } |
| Object[] operations = fOperationList.toArray(); |
| for (int i = 0; i < operations.length; i++) |
| insert(edit, (TextEdit) operations[i]); |
| |
| return new TextEdit[] { edit }; |
| } |
| |
| protected static void insert(TextEdit parent, TextEdit edit) { |
| if (parent.hasChildren() == false) { |
| parent.addChild(edit); |
| if (edit instanceof MoveSourceEdit) { |
| parent.addChild(((MoveSourceEdit) edit).getTargetEdit()); |
| } |
| return; |
| } |
| TextEdit[] children = parent.getChildren(); |
| // First dive down to find the right parent. |
| for (int i = 0; i < children.length; i++) { |
| TextEdit child = children[i]; |
| if (covers(child, edit)) { |
| insert(child, edit); |
| return; |
| } |
| } |
| // We have the right parent. Now check if some of the children have to |
| // be moved under the new edit since it is covering it. |
| for (int i = children.length - 1; i >= 0; i--) { |
| TextEdit child = children[i]; |
| if (covers(edit, child)) { |
| parent.removeChild(i); |
| edit.addChild(child); |
| } |
| } |
| parent.addChild(edit); |
| if (edit instanceof MoveSourceEdit) { |
| parent.addChild(((MoveSourceEdit) edit).getTargetEdit()); |
| } |
| } |
| |
| protected static boolean covers(TextEdit thisEdit, TextEdit otherEdit) {
|
| // an insertion point can't cover anything
|
| if (thisEdit.getLength() == 0) { |
| return false;
|
| } |
| int thisOffset = thisEdit.getOffset(); |
| int thisEnd = thisEdit.getExclusiveEnd(); |
| if (otherEdit.getLength() == 0) { |
| int otherOffset = otherEdit.getOffset(); |
| return thisOffset < otherOffset && otherOffset < thisEnd; |
| } |
| int otherOffset = otherEdit.getOffset(); |
| int otherEnd = otherEdit.getExclusiveEnd(); |
| return thisOffset <= otherOffset && otherEnd <= thisEnd; |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected void deleteNode(IDocumentElementNode node) { |
| // delete previous op on this node, if any |
| TextEdit old = (TextEdit) fOperationTable.get(node); |
| if (old != null) { |
| Object op = fOperationTable.remove(node); |
| fOperationList.remove(op); |
| } |
| // if node has an offset, delete it |
| if (node.getOffset() > -1) { |
| // Create a delete op for this node |
| TextEdit op = getDeleteNodeOperation(node); |
| fOperationTable.put(node, op); |
| fOperationList.add(op); |
| } else if (old == null) { |
| // No previous op on this non-offset node, just rewrite highest ancestor with an offset |
| insertNode(node); |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected void insertNode(IDocumentElementNode node_p) { |
| TextEdit op = null; |
| IDocumentElementNode node = getHighestNodeToBeWritten(node_p); |
| if (node.getParentNode() == null) { |
| // Only add the insertion edit operation if the node is a root node |
| // Otherwise the insertion edit operation will specify to add the |
| // node to the beginning of the file and corrupt it |
| // See Bugs 163161, 166520 |
| if (node.isRoot()) { |
| op = new InsertEdit(0, node.write(true)); |
| } |
| } else { |
| if (node.getOffset() > -1) { |
| // this is an element that was of the form <element/> |
| // it now needs to be broken up into <element><new/></element> |
| op = new ReplaceEdit(node.getOffset(), node.getLength(), node.write(false)); |
| } else { |
| // try to insert after last sibling that has an offset |
| op = insertAfterSibling(node); |
| // insert as first child of its parent |
| if (op == null) { |
| op = insertAsFirstChild(node); |
| } |
| } |
| } |
| // TODO Stephane : patch bug modification de plugin.xml |
| TextEdit old = (TextEdit) fOperationTable.get(node); |
| if (old != null) { |
| fOperationList.remove(old); |
| } |
| // Fin modif Stephane. |
| fOperationTable.put(node, op); |
| fOperationList.add(op); |
| } |
| |
| private InsertEdit insertAfterSibling(IDocumentElementNode node) { |
| IDocumentElementNode sibling = node.getPreviousSibling(); |
| for (;;) { |
| if (sibling == null) { |
| break;
|
| } |
| if (sibling.getOffset() > -1) { |
| node.setLineIndent(sibling.getLineIndent()); |
| return new InsertEdit(sibling.getOffset() + sibling.getLength(), fSep + node.write(true)); |
| } |
| sibling = sibling.getPreviousSibling(); |
| } |
| return null; |
| } |
| |
| private InsertEdit insertAsFirstChild(IDocumentElementNode node) { |
| int offset = node.getParentNode().getOffset(); |
| int length = getNextPosition(fDocument, offset, '>'); |
| node.setLineIndent(node.getParentNode().getLineIndent() + 3); |
| return new InsertEdit(offset + length + 1, fSep + node.write(true)); |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected void modifyNode(IDocumentElementNode node, IModelChangedEvent event) { |
| IDocumentElementNode oldNode = (IDocumentElementNode) event.getOldValue(); |
| IDocumentElementNode newNode = (IDocumentElementNode) event.getNewValue(); |
| IDocumentElementNode node1 = (oldNode.getPreviousSibling() == null || oldNode.equals(newNode.getPreviousSibling())) ? oldNode : newNode; |
| IDocumentElementNode node2 = node1.equals(oldNode) ? newNode : oldNode; |
| if (node1.getOffset() < 0 && node2.getOffset() < 0) { |
| TextEdit op = (TextEdit) fOperationTable.get(node1); |
| if (op == null) { |
| // node 1 has no rule, so node 2 has no rule, therefore rewrite parent/ancestor |
| insertNode(node); |
| } |
| } else if (node1.getOffset() > -1 && node2.getOffset() > -1) { |
| // both nodes have offsets, so create a move target/source combo operation |
| IRegion region = getMoveRegion(node1); |
| MoveSourceEdit source = new MoveSourceEdit(region.getOffset(), region.getLength()); |
| region = getMoveRegion(node2); |
| source.setTargetEdit(new MoveTargetEdit(region.getOffset())); |
| // TODO Stephane : patch bug modification de plugin.xml |
| TextEdit old = (TextEdit) fOperationTable.get(node); |
| if (old != null) { |
| fOperationList.remove(old); |
| } |
| // Fin modif Stephane. |
| fOperationTable.put(node, source); |
| fOperationList.add(source); |
| } else { |
| // one node with offset, the other without offset. Delete/reinsert the one without offset |
| insertNode((node1.getOffset() < 0) ? node1 : node2); |
| } |
| } |
| |
| private IRegion getMoveRegion(IDocumentElementNode node) { |
| int offset = node.getOffset(); |
| int length = node.getLength(); |
| int i = 1; |
| try { |
| for (;; i++) { |
| char ch = fDocument.get(offset - i, 1).toCharArray()[0]; |
| if (Character.isWhitespace(ch) == false) { |
| i -= 1; |
| break; |
| } |
| } |
| } catch (BadLocationException e) { |
| // |
| } |
| return new Region(offset - i, length + i); |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected void addAttributeOperation(IDocumentAttributeNode attr, IModelChangedEvent event) { |
| int offset = attr.getValueOffset(); |
| Object newValue = event.getNewValue(); |
| Object changedObject = attr; |
| TextEdit op = null; |
| if (offset > -1) { |
| if (newValue == null || newValue.toString().length() == 0) { |
| int length = attr.getValueOffset() + attr.getValueLength() + 1 - attr.getNameOffset(); |
| op = getAttributeDeleteEditOperation(attr.getNameOffset(), length); |
| } else { |
| op = new ReplaceEdit(offset, attr.getValueLength(), getWritableString(event.getNewValue().toString())); |
| } |
| } |
| |
| if (op == null) { |
| IDocumentElementNode node = attr.getEnclosingElement(); |
| if (node.getOffset() > -1) { |
| changedObject = node; |
| int len = getNextPosition(fDocument, node.getOffset(), '>'); |
| op = new ReplaceEdit(node.getOffset(), len + 1, node.writeShallow(shouldTerminateElement(fDocument, node.getOffset() + len))); |
| } else { |
| insertNode(node); |
| return; |
| } |
| } |
| // TODO Stephane : patch bug modification de plugin.xml |
| TextEdit old = (TextEdit) fOperationTable.get(changedObject); |
| if (old != null) { |
| fOperationList.remove(old); |
| } |
| // Fin modif Stephane. |
| fOperationTable.put(changedObject, op); |
| fOperationList.add(op); |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected void addElementContentOperation(IDocumentTextNode textNode) { |
| TextEdit op = null; |
| Object changedObject = textNode; |
| if (textNode.getOffset() > -1) { |
| String newText = getWritableString(textNode.getText()); |
| op = new ReplaceEdit(textNode.getOffset(), textNode.getLength(), newText); |
| } else { |
| IDocumentElementNode parent = textNode.getEnclosingElement(); |
| if (parent.getOffset() > -1) { |
| try { |
| String endChars = fDocument.get(parent.getOffset() + parent.getLength() - 2, 2); |
| if ("/>".equals(endChars)) { //$NON-NLS-1$ |
| // parent element is of the form <element/>, rewrite it |
| insertNode(parent); |
| return; |
| } |
| } catch (BadLocationException e) { |
| // |
| } |
| // add text as first child |
| changedObject = parent; |
| StringBuffer buffer = new StringBuffer(fSep); |
| for (int i = 0; i < parent.getLineIndent(); i++) { |
| buffer.append(" "); //$NON-NLS-1$
|
| } |
| buffer.append(" " + getWritableString(textNode.getText())); //$NON-NLS-1$ |
| int offset = parent.getOffset(); |
| int length = getNextPosition(fDocument, offset, '>'); |
| op = new InsertEdit(offset + length + 1, buffer.toString()); |
| } else { |
| insertNode(parent); |
| return; |
| } |
| } |
| // TODO Stephane : patch bug modification de plugin.xml |
| TextEdit old = (TextEdit) fOperationTable.get(changedObject); |
| if (old != null) { |
| fOperationList.remove(old); |
| } |
| // Fin modif Stephane. |
| fOperationTable.put(changedObject, op); |
| fOperationList.add(op); |
| } |
| |
| private boolean shouldTerminateElement(IDocument doc, int offset) { |
| try { |
| return doc.get(offset - 1, 1).toCharArray()[0] == '/'; |
| } catch (BadLocationException e) { |
| // |
| } |
| return false; |
| } |
| |
| private int getNextPosition(IDocument doc, int offset, char ch) { |
| int i = 0; |
| try { |
| for (i = 0; i + offset < doc.getLength(); i++) { |
| if (ch == doc.get(offset + i, 1).toCharArray()[0]) |
| break; |
| } |
| } catch (BadLocationException e) { |
| // |
| } |
| return i; |
| } |
| |
| private DeleteEdit getAttributeDeleteEditOperation(int offset, int length_p) { |
| int length = length_p; |
| try { |
| for (;;) { |
| char ch = fDocument.get(offset + length, 1).toCharArray()[0]; |
| if (!Character.isWhitespace(ch)) { |
| break; |
| } |
| length += 1; |
| } |
| } catch (BadLocationException e) { |
| // |
| } |
| return new DeleteEdit(offset, length); |
| } |
| |
| private DeleteEdit getDeleteNodeOperation(IDocumentElementNode node) { |
| int offset = node.getOffset(); |
| int length = node.getLength(); |
| try { |
| // node starts on this line: |
| int startLine = fDocument.getLineOfOffset(offset); |
| // 1st char on startLine has this offset: |
| int startLineOffset = fDocument.getLineOffset(startLine); |
| // hunt down 1st whitespace/start of line with startOffset: |
| int startOffset; |
| // loop backwards to the beginning of the line, stop if we find non-whitespace |
| for (startOffset = offset - 1; startOffset >= startLineOffset; startOffset -= 1) { |
| if (Character.isWhitespace(fDocument.getChar(startOffset)) == false) { |
| break;
|
| }
|
| } |
| // move forward one (loop stopped after reaching too far) |
| startOffset += 1; |
| // node ends on this line: |
| int endLine = fDocument.getLineOfOffset(offset + length); |
| // length of last line's delimiter: |
| int endLineDelimLength = fDocument.getLineDelimiter(endLine).length(); |
| // hunt last whitespace/end of line with extraLength: |
| int extraLength = length; |
| while (true) { |
| extraLength += 1; |
| if (Character.isWhitespace(fDocument.getChar(offset + extraLength)) == false) { |
| // found non-white space, move back one |
| extraLength -= 1; |
| break; |
| } |
| if (fDocument.getLineOfOffset(offset + extraLength) > endLine) { |
| // don't want to touch the lineDelimeters |
| extraLength -= endLineDelimLength; |
| break; |
| } |
| } |
| |
| // if we reached start of line, remove newline |
| if (startOffset == startLineOffset) { |
| startOffset -= fDocument.getLineDelimiter(startLine).length();
|
| } |
| // add difference of new offset |
| length = extraLength + (offset - startOffset); |
| offset = startOffset; |
| // printDeletionRange(offset, length); |
| } catch (BadLocationException e) { |
| // |
| } |
| return new DeleteEdit(offset, length); |
| } |
| |
| protected void printDeletionRange(int offset, int length) { |
| try { |
| // newlines printed as \n |
| // carriage returns printed as \r |
| // tabs printed as \t |
| // spaces printed as * |
| String string = fDocument.get(offset, length); |
| StringBuffer buffer = new StringBuffer(); |
| for (int i = 0; i < string.length(); i++) { |
| char c = string.charAt(i); |
| if (c == '\n') |
| buffer.append("\\n"); //$NON-NLS-1$ |
| else if (c == '\r') |
| buffer.append("\\r"); //$NON-NLS-1$ |
| else if (c == '\t') |
| buffer.append("\\t"); //$NON-NLS-1$ |
| else if (c == ' ') |
| buffer.append('*'); |
| else |
| buffer.append(c); |
| } |
| System.out.println(buffer.toString()); |
| } catch (BadLocationException e) { |
| // |
| } |
| } |
| |
| private IDocumentElementNode getHighestNodeToBeWritten(IDocumentElementNode node) { |
| IDocumentElementNode parent = node.getParentNode(); |
| if (parent == null) { |
| return node;
|
| } |
| if (parent.getOffset() > -1) { |
| try { |
| String endChars = fDocument.get(parent.getOffset() + parent.getLength() - 2, 2); |
| return ("/>".equals(endChars)) ? parent : node; //$NON-NLS-1$ |
| } catch (BadLocationException e) { |
| return node; |
| } |
| } |
| return getHighestNodeToBeWritten(parent); |
| } |
| |
| private String getWritableString(String source) { |
| return PDEXMLHelper.getWritableString(source); |
| } |
| |
| public void modelChanged(IModelChangedEvent event) { |
| Object[] objects = event.getChangedObjects(); |
| if (objects == null) { |
| return;
|
| } |
| for (int i = 0; i < objects.length; i++) { |
| if (objects[i] instanceof IDocumentElementNode == false) { |
| continue;
|
| } |
| IDocumentElementNode node = (IDocumentElementNode) objects[i]; |
| Object op = fOperationTable.remove(node); |
| fOperationList.remove(op); |
| switch (event.getChangeType()) { |
| case IModelChangedEvent.REMOVE: |
| deleteNode(node); |
| break; |
| case IModelChangedEvent.INSERT: |
| insertNode(node); |
| break; |
| case IModelChangedEvent.CHANGE: |
| IDocumentAttributeNode attr = node.getDocumentAttribute(event.getChangedProperty()); |
| if (attr != null) { |
| addAttributeOperation(attr, event); |
| } else { |
| if (event.getOldValue() instanceof IDocumentTextNode) { |
| addElementContentOperation((IDocumentTextNode) event.getOldValue()); |
| } else if (event.getOldValue() instanceof IDocumentElementNode && event.getNewValue() instanceof IDocumentElementNode) { |
| // swapping of nodes |
| modifyNode(node, event); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * @see org.eclipse.pde.internal.core.text.IModelTextChangeListener#getReadableName(org.eclipse.text.edits.TextEdit) |
| */ |
| public String getReadableName(TextEdit edit_p) { |
| return EGFCommonConstants.EMPTY_STRING; |
| }
|
| |
| } |