001: /*******************************************************************************
002: * Copyright (c) 2005, 2007 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.pde.internal.core.text.plugin;
011:
012: import java.util.ArrayList;
013: import java.util.HashMap;
014:
015: import org.eclipse.jface.text.BadLocationException;
016: import org.eclipse.jface.text.IDocument;
017: import org.eclipse.jface.text.IRegion;
018: import org.eclipse.jface.text.Region;
019: import org.eclipse.osgi.util.NLS;
020: import org.eclipse.pde.core.IModelChangedEvent;
021: import org.eclipse.pde.internal.core.PDECoreMessages;
022: import org.eclipse.pde.internal.core.text.AbstractTextChangeListener;
023: import org.eclipse.pde.internal.core.text.IDocumentAttributeNode;
024: import org.eclipse.pde.internal.core.text.IDocumentElementNode;
025: import org.eclipse.pde.internal.core.text.IDocumentTextNode;
026: import org.eclipse.pde.internal.core.util.PDEXMLHelper;
027: import org.eclipse.text.edits.DeleteEdit;
028: import org.eclipse.text.edits.InsertEdit;
029: import org.eclipse.text.edits.MoveSourceEdit;
030: import org.eclipse.text.edits.MoveTargetEdit;
031: import org.eclipse.text.edits.ReplaceEdit;
032: import org.eclipse.text.edits.TextEdit;
033:
034: public class XMLTextChangeListener extends AbstractTextChangeListener {
035:
036: private ArrayList fOperationList = new ArrayList();
037: private HashMap fReadableNames = null;
038:
039: public XMLTextChangeListener(IDocument document) {
040: this (document, false);
041: }
042:
043: public XMLTextChangeListener(IDocument document,
044: boolean generateReadableNames) {
045: super (document);
046: // if we are not generating names, leave the HashMap null
047: // this way a null test on the map can be used to determine if names should be generated
048: if (generateReadableNames)
049: fReadableNames = new HashMap();
050: }
051:
052: public TextEdit[] getTextOperations() {
053: if (fOperationList.size() == 0)
054: return new TextEdit[0];
055: return (TextEdit[]) fOperationList
056: .toArray(new TextEdit[fOperationList.size()]);
057: }
058:
059: protected static void insert(TextEdit parent, TextEdit edit) {
060: if (!parent.hasChildren()) {
061: parent.addChild(edit);
062: if (edit instanceof MoveSourceEdit) {
063: parent
064: .addChild(((MoveSourceEdit) edit)
065: .getTargetEdit());
066: }
067: return;
068: }
069: TextEdit[] children = parent.getChildren();
070: // First dive down to find the right parent.
071: for (int i = 0; i < children.length; i++) {
072: TextEdit child = children[i];
073: if (covers(child, edit)) {
074: insert(child, edit);
075: return;
076: }
077: }
078: // We have the right parent. Now check if some of the children have to
079: // be moved under the new edit since it is covering it.
080: for (int i = children.length - 1; i >= 0; i--) {
081: TextEdit child = children[i];
082: if (covers(edit, child)) {
083: parent.removeChild(i);
084: edit.addChild(child);
085: }
086: }
087: parent.addChild(edit);
088: if (edit instanceof MoveSourceEdit) {
089: parent.addChild(((MoveSourceEdit) edit).getTargetEdit());
090: }
091: }
092:
093: protected static boolean covers(TextEdit this Edit,
094: TextEdit otherEdit) {
095: if (this Edit.getLength() == 0) // an insertion point can't cover anything
096: return false;
097:
098: int this Offset = this Edit.getOffset();
099: int this End = this Edit.getExclusiveEnd();
100: if (otherEdit.getLength() == 0) {
101: int otherOffset = otherEdit.getOffset();
102: return this Offset < otherOffset && otherOffset < this End;
103: }
104: int otherOffset = otherEdit.getOffset();
105: int otherEnd = otherEdit.getExclusiveEnd();
106: return this Offset <= otherOffset && otherEnd <= this End;
107: }
108:
109: protected void deleteNode(IDocumentElementNode node) {
110: // delete previous op on this node, if any
111: TextEdit old = (TextEdit) fOperationTable.get(node);
112: if (old != null) {
113: Object op = fOperationTable.remove(node);
114: fOperationList.remove(op);
115: if (fReadableNames != null)
116: fReadableNames.remove(op);
117: }
118:
119: // if node has an offset, delete it
120: if (node.getOffset() > -1) {
121: // Create a delete op for this node
122: TextEdit op = getDeleteNodeOperation(node);
123: fOperationTable.put(node, op);
124: fOperationList.add(op);
125: if (fReadableNames != null)
126: fReadableNames
127: .put(
128: op,
129: NLS
130: .bind(
131: PDECoreMessages.XMLTextChangeListener_editNames_removeNode,
132: node.getXMLTagName()));
133: } else if (old == null) {
134: // No previous op on this non-offset node, just rewrite highest ancestor with an offset
135: insertNode(node);
136: }
137: }
138:
139: protected void insertNode(IDocumentElementNode node) {
140: TextEdit op = null;
141: node = getHighestNodeToBeWritten(node);
142: if (node.getParentNode() == null) {
143: // Only add the insertion edit operation if the node is a root node
144: // Otherwise the insertion edit operation will specify to add the
145: // node to the beginning of the file and corrupt it
146: // See Bugs 163161, 166520
147: if (node.isRoot()) {
148: op = new InsertEdit(0, node.write(true));
149: }
150: } else {
151: if (node.getOffset() > -1) {
152: // this is an element that was of the form <element/>
153: // it now needs to be broken up into <element><new/></element>
154: op = new ReplaceEdit(node.getOffset(),
155: node.getLength(), node.write(false));
156: } else {
157: // try to insert after last sibling that has an offset
158: op = insertAfterSibling(node);
159: // insert as first child of its parent
160: if (op == null) {
161: op = insertAsFirstChild(node);
162: }
163: }
164: }
165: fOperationTable.put(node, op);
166: fOperationList.add(op);
167: if (fReadableNames != null)
168: fReadableNames
169: .put(
170: op,
171: NLS
172: .bind(
173: PDECoreMessages.XMLTextChangeListener_editNames_insertNode,
174: node.getXMLTagName()));
175: }
176:
177: private InsertEdit insertAfterSibling(IDocumentElementNode node) {
178: IDocumentElementNode sibling = node.getPreviousSibling();
179: for (;;) {
180: if (sibling == null)
181: break;
182: if (sibling.getOffset() > -1) {
183: node.setLineIndent(sibling.getLineIndent());
184: return new InsertEdit(sibling.getOffset()
185: + sibling.getLength(), fSep + node.write(true));
186: }
187: sibling = sibling.getPreviousSibling();
188: }
189: return null;
190: }
191:
192: private InsertEdit insertAsFirstChild(IDocumentElementNode node) {
193: int offset = node.getParentNode().getOffset();
194: int length = getNextPosition(fDocument, offset, '>');
195: node.setLineIndent(node.getParentNode().getLineIndent() + 3);
196: return new InsertEdit(offset + length + 1, fSep
197: + node.write(true));
198: }
199:
200: protected void modifyNode(IDocumentElementNode node,
201: IModelChangedEvent event) {
202: IDocumentElementNode oldNode = (IDocumentElementNode) event
203: .getOldValue();
204: IDocumentElementNode newNode = (IDocumentElementNode) event
205: .getNewValue();
206:
207: IDocumentElementNode node1 = (oldNode.getPreviousSibling() == null || oldNode
208: .equals(newNode.getPreviousSibling())) ? oldNode
209: : newNode;
210: IDocumentElementNode node2 = node1.equals(oldNode) ? newNode
211: : oldNode;
212:
213: if (node1.getOffset() < 0 && node2.getOffset() < 0) {
214: TextEdit op = (TextEdit) fOperationTable.get(node1);
215: if (op == null) {
216: // node 1 has no rule, so node 2 has no rule, therefore rewrite parent/ancestor
217: insertNode(node);
218: }
219: } else if (node1.getOffset() > -1 && node2.getOffset() > -1) {
220: // both nodes have offsets, so create a move target/source combo operation
221: IRegion region = getMoveRegion(node1);
222: MoveSourceEdit source = new MoveSourceEdit(region
223: .getOffset(), region.getLength());
224: region = getMoveRegion(node2);
225: source
226: .setTargetEdit(new MoveTargetEdit(region
227: .getOffset()));
228: fOperationTable.put(node, source);
229: fOperationList.add(source);
230: if (fReadableNames != null)
231: fReadableNames
232: .put(
233: source,
234: NLS
235: .bind(
236: PDECoreMessages.XMLTextChangeListener_editNames_modifyNode,
237: node.getXMLTagName()));
238: } else {
239: // one node with offset, the other without offset. Delete/reinsert the one without offset
240: insertNode((node1.getOffset() < 0) ? node1 : node2);
241: }
242: }
243:
244: private IRegion getMoveRegion(IDocumentElementNode node) {
245: int offset = node.getOffset();
246: int length = node.getLength();
247: int i = 1;
248: try {
249: for (;; i++) {
250: char ch = fDocument.get(offset - i, 1).toCharArray()[0];
251: if (!Character.isWhitespace(ch)) {
252: i -= 1;
253: break;
254: }
255: }
256: } catch (BadLocationException e) {
257: }
258: return new Region(offset - i, length + i);
259: }
260:
261: protected void addAttributeOperation(IDocumentAttributeNode attr,
262: IModelChangedEvent event) {
263: int offset = attr.getValueOffset();
264: Object newValue = event.getNewValue();
265: Object changedObject = attr;
266: String name = null;
267: TextEdit op = null;
268: if (offset > -1) {
269: if (newValue == null || newValue.toString().length() == 0) {
270: int length = attr.getValueOffset()
271: + attr.getValueLength() + 1
272: - attr.getNameOffset();
273: op = getAttributeDeleteEditOperation(attr
274: .getNameOffset(), length);
275: if (fReadableNames != null)
276: name = NLS
277: .bind(
278: PDECoreMessages.XMLTextChangeListener_editNames_removeAttribute,
279: new String[] {
280: attr.getAttributeName(),
281: attr.getEnclosingElement()
282: .getXMLTagName() });
283: } else {
284: op = new ReplaceEdit(offset, attr.getValueLength(),
285: getWritableString(event.getNewValue()
286: .toString()));
287: if (fReadableNames != null)
288: name = NLS
289: .bind(
290: PDECoreMessages.XMLTextChangeListener_editNames_modifyAttribute,
291: new String[] {
292: attr.getAttributeName(),
293: attr.getEnclosingElement()
294: .getXMLTagName() });
295: }
296: }
297:
298: if (op == null) {
299: IDocumentElementNode node = attr.getEnclosingElement();
300: if (node.getOffset() > -1) {
301: changedObject = node;
302: int len = getNextPosition(fDocument, node.getOffset(),
303: '>');
304: op = new ReplaceEdit(node.getOffset(), len + 1, node
305: .writeShallow(shouldTerminateElement(fDocument,
306: node.getOffset() + len)));
307: if (fReadableNames != null)
308: name = NLS
309: .bind(
310: PDECoreMessages.XMLTextChangeListener_editNames_addAttribute,
311: new String[] {
312: attr.getAttributeName(),
313: attr.getEnclosingElement()
314: .getXMLTagName() });
315: } else {
316: insertNode(node);
317: return;
318: }
319: }
320: fOperationTable.put(changedObject, op);
321: fOperationList.add(op);
322: if (fReadableNames != null && name != null)
323: fReadableNames.put(op, name);
324: }
325:
326: protected void addElementContentOperation(IDocumentTextNode textNode) {
327: TextEdit op = null;
328: Object changedObject = textNode;
329: if (textNode.getOffset() > -1) {
330: String newText = getWritableString(textNode.getText());
331: op = new ReplaceEdit(textNode.getOffset(), textNode
332: .getLength(), newText);
333: } else {
334: IDocumentElementNode parent = textNode
335: .getEnclosingElement();
336: if (parent.getOffset() > -1) {
337: try {
338: String endChars = fDocument.get(parent.getOffset()
339: + parent.getLength() - 2, 2);
340: if ("/>".equals(endChars)) { //$NON-NLS-1$
341: // parent element is of the form <element/>, rewrite it
342: insertNode(parent);
343: return;
344: }
345: } catch (BadLocationException e) {
346: }
347: // add text as first child
348: changedObject = parent;
349: StringBuffer buffer = new StringBuffer(fSep);
350: for (int i = 0; i < parent.getLineIndent(); i++)
351: buffer.append(" "); //$NON-NLS-1$
352: buffer
353: .append(" " + getWritableString(textNode.getText())); //$NON-NLS-1$
354: int offset = parent.getOffset();
355: int length = getNextPosition(fDocument, offset, '>');
356: op = new InsertEdit(offset + length + 1, buffer
357: .toString());
358: } else {
359: insertNode(parent);
360: return;
361: }
362: }
363: fOperationTable.put(changedObject, op);
364: fOperationList.add(op);
365: if (fReadableNames != null)
366: fReadableNames
367: .put(
368: op,
369: NLS
370: .bind(
371: PDECoreMessages.XMLTextChangeListener_editNames_addContent,
372: textNode
373: .getEnclosingElement()
374: .getXMLTagName()));
375: }
376:
377: private boolean shouldTerminateElement(IDocument doc, int offset) {
378: try {
379: return doc.get(offset - 1, 1).toCharArray()[0] == '/';
380: } catch (BadLocationException e) {
381: }
382: return false;
383: }
384:
385: private int getNextPosition(IDocument doc, int offset, char ch) {
386: int i = 0;
387: try {
388: for (i = 0; i + offset < doc.getLength(); i++) {
389: if (ch == doc.get(offset + i, 1).toCharArray()[0])
390: break;
391: }
392: } catch (BadLocationException e) {
393: }
394: return i;
395: }
396:
397: private DeleteEdit getAttributeDeleteEditOperation(int offset,
398: int length) {
399: try {
400: for (;;) {
401: char ch = fDocument.get(offset + length, 1)
402: .toCharArray()[0];
403: if (!Character.isWhitespace(ch)) {
404: break;
405: }
406:
407: length += 1;
408: }
409: } catch (BadLocationException e) {
410: }
411: return new DeleteEdit(offset, length);
412: }
413:
414: private DeleteEdit getDeleteNodeOperation(IDocumentElementNode node) {
415: int offset = node.getOffset();
416: int length = node.getLength();
417: try {
418: // node starts on this line:
419: int startLine = fDocument.getLineOfOffset(offset);
420: // 1st char on startLine has this offset:
421: int startLineOffset = fDocument.getLineOffset(startLine);
422: // hunt down 1st whitespace/start of line with startOffset:
423: int startOffset;
424: // loop backwards to the beginning of the line, stop if we find non-whitespace
425: for (startOffset = offset - 1; startOffset >= startLineOffset; startOffset -= 1)
426: if (!Character.isWhitespace(fDocument
427: .getChar(startOffset)))
428: break;
429:
430: // move forward one (loop stopped after reaching too far)
431: startOffset += 1;
432:
433: // node ends on this line:
434: int endLine = fDocument.getLineOfOffset(offset + length);
435: // length of last line's delimiter:
436: int endLineDelimLength = fDocument
437: .getLineDelimiter(endLine).length();
438: // hunt last whitespace/end of line with extraLength:
439: int extraLength = length;
440: while (true) {
441: extraLength += 1;
442: if (!Character.isWhitespace(fDocument.getChar(offset
443: + extraLength))) {
444: // found non-white space, move back one
445: extraLength -= 1;
446: break;
447: }
448: if (fDocument.getLineOfOffset(offset + extraLength) > endLine) {
449: // don't want to touch the lineDelimeters
450: extraLength -= endLineDelimLength;
451: break;
452: }
453: }
454:
455: // if we reached start of line, remove newline
456: if (startOffset == startLineOffset)
457: startOffset -= fDocument.getLineDelimiter(startLine)
458: .length();
459:
460: // add difference of new offset
461: length = extraLength + (offset - startOffset);
462: offset = startOffset;
463: // printDeletionRange(offset, length);
464: } catch (BadLocationException e) {
465: }
466: return new DeleteEdit(offset, length);
467: }
468:
469: protected void printDeletionRange(int offset, int length) {
470: try {
471: // newlines printed as \n
472: // carriage returns printed as \r
473: // tabs printed as \t
474: // spaces printed as *
475: String string = fDocument.get(offset, length);
476: StringBuffer buffer = new StringBuffer();
477: for (int i = 0; i < string.length(); i++) {
478: char c = string.charAt(i);
479: if (c == '\n')
480: buffer.append("\\n"); //$NON-NLS-1$
481: else if (c == '\r')
482: buffer.append("\\r"); //$NON-NLS-1$
483: else if (c == '\t')
484: buffer.append("\\t"); //$NON-NLS-1$
485: else if (c == ' ')
486: buffer.append('*');
487: else
488: buffer.append(c);
489: }
490: System.out.println(buffer.toString());
491: } catch (BadLocationException e) {
492: }
493: }
494:
495: private IDocumentElementNode getHighestNodeToBeWritten(
496: IDocumentElementNode node) {
497: IDocumentElementNode parent = node.getParentNode();
498: if (parent == null)
499: return node;
500: if (parent.getOffset() > -1) {
501: try {
502: String endChars = fDocument.get(parent.getOffset()
503: + parent.getLength() - 2, 2);
504: return ("/>".equals(endChars)) ? parent : node; //$NON-NLS-1$
505: } catch (BadLocationException e) {
506: return node;
507: }
508:
509: }
510: return getHighestNodeToBeWritten(parent);
511: }
512:
513: private String getWritableString(String source) {
514: return PDEXMLHelper.getWritableString(source);
515: }
516:
517: public void modelChanged(IModelChangedEvent event) {
518: Object[] objects = event.getChangedObjects();
519: if (objects == null)
520: return;
521: for (int i = 0; i < objects.length; i++) {
522: if (!(objects[i] instanceof IDocumentElementNode))
523: continue;
524: IDocumentElementNode node = (IDocumentElementNode) objects[i];
525: Object op = fOperationTable.remove(node);
526: fOperationList.remove(op);
527: if (fReadableNames != null)
528: fReadableNames.remove(op);
529: switch (event.getChangeType()) {
530: case IModelChangedEvent.REMOVE:
531: deleteNode(node);
532: break;
533: case IModelChangedEvent.INSERT:
534: insertNode(node);
535: break;
536: case IModelChangedEvent.CHANGE:
537: IDocumentAttributeNode attr = node
538: .getDocumentAttribute(event
539: .getChangedProperty());
540: if (attr != null) {
541: addAttributeOperation(attr, event);
542: } else {
543: if (event.getOldValue() instanceof IDocumentTextNode) {
544: addElementContentOperation((IDocumentTextNode) event
545: .getOldValue());
546: } else if (event.getOldValue() instanceof IDocumentElementNode
547: && event.getNewValue() instanceof IDocumentElementNode) {
548: // swapping of nodes
549: modifyNode(node, event);
550: }
551: }
552: }
553: }
554: }
555:
556: public String getReadableName(TextEdit edit) {
557: if (fReadableNames != null && fReadableNames.containsKey(edit))
558: return (String) fReadableNames.get(edit);
559: return null;
560: }
561: }
|