blob: b56cb94e7a9f8d493560175f744636827148b8db [file] [log] [blame]
/**
* 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;
}
}