001: /*******************************************************************************
002: * Copyright (c) 2003, 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.ui.editor.context;
011:
012: import java.util.ArrayList;
013: import java.util.HashMap;
014: import java.util.Iterator;
015:
016: import org.eclipse.core.filebuffers.IDocumentSetupParticipant;
017: import org.eclipse.jface.text.BadLocationException;
018: import org.eclipse.jface.text.IDocument;
019: import org.eclipse.jface.text.IRegion;
020: import org.eclipse.jface.text.Region;
021: import org.eclipse.jface.text.TextUtilities;
022: import org.eclipse.pde.core.IModelChangedEvent;
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.pde.internal.ui.editor.PDEFormEditor;
028: import org.eclipse.text.edits.DeleteEdit;
029: import org.eclipse.text.edits.InsertEdit;
030: import org.eclipse.text.edits.MoveSourceEdit;
031: import org.eclipse.text.edits.MoveTargetEdit;
032: import org.eclipse.text.edits.ReplaceEdit;
033: import org.eclipse.text.edits.TextEdit;
034: import org.eclipse.ui.IEditorInput;
035:
036: public abstract class XMLInputContext extends UTF8InputContext {
037: protected HashMap fOperationTable = new HashMap();
038: protected HashMap fMoveOperations = new HashMap();
039:
040: /**
041: * @param editor
042: * @param input
043: */
044: public XMLInputContext(PDEFormEditor editor, IEditorInput input,
045: boolean primary) {
046: super (editor, input, primary);
047: }
048:
049: protected IDocumentSetupParticipant getDocumentSetupParticipant() {
050: return new XMLDocumentSetupParticpant();
051: }
052:
053: /* (non-Javadoc)
054: * @see org.eclipse.pde.internal.ui.neweditor.context.InputContext#addTextEditOperation(java.util.ArrayList, org.eclipse.pde.core.IModelChangedEvent)
055: */
056: protected void addTextEditOperation(ArrayList ops,
057: IModelChangedEvent event) {
058: Object[] objects = event.getChangedObjects();
059: if (objects != null) {
060: for (int i = 0; i < objects.length; i++) {
061: Object object = objects[i];
062: switch (event.getChangeType()) {
063: case IModelChangedEvent.REMOVE:
064: if (object instanceof IDocumentElementNode)
065: removeNode((IDocumentElementNode) object, ops);
066: break;
067: case IModelChangedEvent.INSERT:
068: if (object instanceof IDocumentElementNode)
069: insertNode((IDocumentElementNode) object, ops);
070: break;
071: case IModelChangedEvent.CHANGE:
072: if (object instanceof IDocumentElementNode) {
073: IDocumentElementNode node = (IDocumentElementNode) object;
074: IDocumentAttributeNode attr = node
075: .getDocumentAttribute(event
076: .getChangedProperty());
077: if (attr != null) {
078: addAttributeOperation(attr, ops, event);
079: } else if (event.getOldValue() instanceof IDocumentElementNode
080: && event.getNewValue() instanceof IDocumentElementNode) {
081: // swapping of nodes
082: modifyNode(node, ops, event);
083: }
084: } else if (object instanceof IDocumentTextNode) {
085: addElementContentOperation(
086: (IDocumentTextNode) object, ops);
087: }
088: default:
089: break;
090: }
091: }
092: }
093: }
094:
095: private void removeNode(IDocumentElementNode node, ArrayList ops) {
096: // delete previous op on this node, if any
097: TextEdit old = (TextEdit) fOperationTable.get(node);
098: if (old != null) {
099: ops.remove(old);
100: fOperationTable.remove(node);
101: }
102: TextEdit oldMove = (TextEdit) fMoveOperations.get(node);
103: if (oldMove != null) {
104: ops.remove(oldMove);
105: fMoveOperations.remove(node);
106: }
107: // if node has an offset, delete it
108: if (node.getOffset() > -1) {
109: // Create a delete op for this node
110: TextEdit op = getDeleteNodeOperation(node);
111: ops.add(op);
112: fOperationTable.put(node, op);
113: } else if (old == null && oldMove == null) {
114: // No previous op on this non-offset node, just rewrite highest ancestor with an offset
115: insertNode(node, ops);
116: }
117: }
118:
119: private void insertNode(IDocumentElementNode node, ArrayList ops) {
120: TextEdit op = null;
121: node = getHighestNodeToBeWritten(node);
122: if (node.getParentNode() == null) {
123: // https://bugs.eclipse.org/bugs/show_bug.cgi?id=163161
124: if (node.isRoot())
125: op = new InsertEdit(0, node.write(true));
126: } else {
127: if (node.getOffset() > -1) {
128: // this is an element that was of the form <element/>
129: // it now needs to be broken up into <element><new/></element>
130: op = new ReplaceEdit(node.getOffset(),
131: node.getLength(), node.write(false));
132: } else {
133: // try to insert after last sibling that has an offset
134: op = insertAfterSibling(node);
135: // insert as first child of its parent
136: if (op == null) {
137: op = insertAsFirstChild(node);
138: }
139: }
140: }
141: TextEdit old = (TextEdit) fOperationTable.get(node);
142: if (old != null)
143: ops.remove(old);
144: if (op != null) {
145: ops.add(op);
146: fOperationTable.put(node, op);
147: }
148: }
149:
150: private InsertEdit insertAfterSibling(IDocumentElementNode node) {
151: IDocumentElementNode sibling = node.getPreviousSibling();
152: for (;;) {
153: if (sibling == null)
154: break;
155: if (sibling.getOffset() > -1) {
156: node.setLineIndent(sibling.getLineIndent());
157: String sep = TextUtilities
158: .getDefaultLineDelimiter(getDocumentProvider()
159: .getDocument(getInput()));
160: return new InsertEdit(sibling.getOffset()
161: + sibling.getLength(), sep + node.write(true));
162: }
163: sibling = sibling.getPreviousSibling();
164: }
165: return null;
166: }
167:
168: private InsertEdit insertAsFirstChild(IDocumentElementNode node) {
169: int offset = node.getParentNode().getOffset();
170: int length = getNextPosition(getDocumentProvider().getDocument(
171: getInput()), offset, '>');
172: node.setLineIndent(node.getParentNode().getLineIndent() + 3);
173: String sep = TextUtilities
174: .getDefaultLineDelimiter(getDocumentProvider()
175: .getDocument(getInput()));
176: return new InsertEdit(offset + length + 1, sep
177: + node.write(true));
178: }
179:
180: private void modifyNode(IDocumentElementNode node, ArrayList ops,
181: IModelChangedEvent event) {
182: IDocumentElementNode oldNode = (IDocumentElementNode) event
183: .getOldValue();
184: IDocumentElementNode newNode = (IDocumentElementNode) event
185: .getNewValue();
186:
187: IDocumentElementNode node1 = (oldNode.getPreviousSibling() == null || oldNode
188: .equals(newNode.getPreviousSibling())) ? oldNode
189: : newNode;
190: IDocumentElementNode node2 = node1.equals(oldNode) ? newNode
191: : oldNode;
192:
193: if (node1.getOffset() < 0 && node2.getOffset() < 2) {
194: TextEdit op = (TextEdit) fOperationTable.get(node1);
195: if (op == null) {
196: // node 1 has no rule, so node 2 has no rule, therefore rewrite parent/ancestor
197: insertNode(node, ops);
198: } else {
199: // swap order of insert operations
200: TextEdit op2 = (TextEdit) fOperationTable.get(node2);
201: ops.set(ops.indexOf(op), op2);
202: ops.set(ops.indexOf(op2), op);
203: }
204: } else if (node1.getOffset() > -1 && node2.getOffset() > -1) {
205: // both nodes have offsets, so create a move target/source combo operation
206: IRegion region = getMoveRegion(node1);
207: MoveSourceEdit source = new MoveSourceEdit(region
208: .getOffset(), region.getLength());
209: region = getMoveRegion(node2);
210: source
211: .setTargetEdit(new MoveTargetEdit(region
212: .getOffset()));
213: MoveSourceEdit op = (MoveSourceEdit) fMoveOperations
214: .get(node1);
215: if (op != null) {
216: ops.set(ops.indexOf(op), source);
217: } else {
218: op = (MoveSourceEdit) fMoveOperations.get(node2);
219: if (op != null
220: && op.getTargetEdit().getOffset() == source
221: .getOffset()) {
222: fMoveOperations.remove(node2);
223: ops.remove(op);
224: return;
225: }
226: ops.add(source);
227: }
228: fMoveOperations.put(node1, source);
229: } else {
230: // one node with offset, the other without offset. Delete/reinsert the one without offset
231: insertNode((node1.getOffset() < 0) ? node1 : node2, ops);
232: }
233: }
234:
235: private IRegion getMoveRegion(IDocumentElementNode node) {
236: int offset = node.getOffset();
237: int length = node.getLength();
238: int i = 1;
239: try {
240: IDocument doc = getDocumentProvider().getDocument(
241: getInput());
242: for (;; i++) {
243: char ch = doc.get(offset - i, 1).toCharArray()[0];
244: if (!Character.isWhitespace(ch)) {
245: i -= 1;
246: break;
247: }
248: }
249: } catch (BadLocationException e) {
250: }
251: return new Region(offset - i, length + i);
252: }
253:
254: private void addAttributeOperation(IDocumentAttributeNode attr,
255: ArrayList ops, IModelChangedEvent event) {
256: int offset = attr.getValueOffset();
257: Object newValue = event.getNewValue();
258: Object changedObject = attr;
259: TextEdit op = null;
260: if (offset > -1) {
261: if (newValue == null || newValue.toString().length() == 0) {
262: int length = attr.getValueOffset()
263: + attr.getValueLength() + 1
264: - attr.getNameOffset();
265: op = getAttributeDeleteEditOperation(attr
266: .getNameOffset(), length);
267: } else {
268: op = new ReplaceEdit(offset, attr.getValueLength(),
269: getWritableAttributeNodeValue(event
270: .getNewValue().toString()));
271: }
272: }
273:
274: if (op == null) {
275: IDocumentElementNode node = attr.getEnclosingElement();
276: IDocument doc = getDocumentProvider().getDocument(
277: getInput());
278: if (node.getOffset() > -1) {
279: changedObject = node;
280: int len = getNextPosition(doc, node.getOffset(), '>');
281: op = new ReplaceEdit(node.getOffset(), len + 1, node
282: .writeShallow(shouldTerminateElement(doc, node
283: .getOffset()
284: + len)));
285: } else {
286: insertNode(node, ops);
287: return;
288: }
289: }
290: TextEdit oldOp = (TextEdit) fOperationTable.get(changedObject);
291: if (oldOp != null)
292: ops.remove(oldOp);
293: ops.add(op);
294: fOperationTable.put(changedObject, op);
295: }
296:
297: private void addElementContentOperation(IDocumentTextNode textNode,
298: ArrayList ops) {
299: TextEdit op = null;
300: Object changedObject = textNode;
301: if (textNode.getOffset() > -1) {
302: String newText = getWritableTextNodeString(textNode);
303: op = new ReplaceEdit(textNode.getOffset(), textNode
304: .getLength(), newText);
305: } else {
306: IDocumentElementNode parent = textNode
307: .getEnclosingElement();
308: if (parent.getOffset() > -1) {
309: IDocument doc = getDocumentProvider().getDocument(
310: getInput());
311: try {
312: String endChars = doc.get(parent.getOffset()
313: + parent.getLength() - 2, 2);
314: if ("/>".equals(endChars)) { //$NON-NLS-1$
315: // parent element is of the form <element/>, rewrite it
316: insertNode(parent, ops);
317: return;
318: }
319: } catch (BadLocationException e) {
320: }
321: // add text as first child
322: changedObject = parent;
323: String sep = TextUtilities
324: .getDefaultLineDelimiter(getDocumentProvider()
325: .getDocument(getInput()));
326: StringBuffer buffer = new StringBuffer(sep);
327: for (int i = 0; i < parent.getLineIndent(); i++)
328: buffer.append(" "); //$NON-NLS-1$
329: buffer
330: .append(" " + getWritableTextNodeString(textNode)); //$NON-NLS-1$
331: int offset = parent.getOffset();
332: int length = getNextPosition(doc, offset, '>');
333: op = new InsertEdit(offset + length + 1, buffer
334: .toString());
335: } else {
336: insertNode(parent, ops);
337: return;
338: }
339: }
340: TextEdit oldOp = (TextEdit) fOperationTable.get(changedObject);
341: if (oldOp != null)
342: ops.remove(oldOp);
343: ops.add(op);
344: fOperationTable.put(changedObject, op);
345: }
346:
347: private boolean shouldTerminateElement(IDocument doc, int offset) {
348: try {
349: return doc.get(offset - 1, 1).toCharArray()[0] == '/';
350: } catch (BadLocationException e) {
351: }
352: return false;
353: }
354:
355: private int getNextPosition(IDocument doc, int offset, char ch) {
356: int i = 0;
357: try {
358: for (i = 0; i + offset < doc.getLength(); i++) {
359: if (ch == doc.getChar(offset + i))
360: break;
361: }
362: } catch (BadLocationException e) {
363: }
364: return i;
365: }
366:
367: private DeleteEdit getAttributeDeleteEditOperation(int offset,
368: int length) {
369: try {
370: IDocument doc = getDocumentProvider().getDocument(
371: getInput());
372: // Traverse backwards in the document starting at the attribute
373: // offset
374: // Goal: Delete all whitespace preceding the attribute name
375: // including spaces, tabs and newlines
376: // We want the next attribute (if defined) to be in the same
377: // position as the deleted one and properly indented. Otherwise,
378: // we want the open angle bracket to be adjacent to the start
379: // element tag name or adjacent to the previous attribute (if
380: // defined) before the deleted one
381: // e.g. _____\n________att1="value1"
382: // This is accomplished by growing the length and decrementing
383: // the offset in order to include the extra whitespace in the
384: // deletion operation
385: for (int i = (offset - 1); i >= 0; i--) {
386: // Get the character at the specified document index
387: char character = doc.getChar(i);
388: // If the character is whitespace, include it in the deletion
389: // operation
390: if (Character.isWhitespace(character)) {
391: // Grow length by one
392: length = length + 1;
393: // Decrement offset by one
394: offset = offset - 1;
395: } else {
396: // Non-whitespace character encountered, do not mark it
397: // for deletion and we are done
398: break;
399: }
400: }
401: } catch (BadLocationException e) {
402: }
403: return new DeleteEdit(offset, length);
404: }
405:
406: private DeleteEdit getDeleteNodeOperation(IDocumentElementNode node) {
407: int offset = node.getOffset();
408: int length = node.getLength();
409: try {
410: IDocument doc = getDocumentProvider().getDocument(
411: getInput());
412: // node starts on this line:
413: int startLine = doc.getLineOfOffset(offset);
414: // 1st char on startLine has this offset:
415: int startLineOffset = doc.getLineOffset(startLine);
416: // hunt down 1st whitespace/start of line with startOffset:
417: int startOffset;
418: // loop backwards to the beginning of the line, stop if we find non-whitespace
419: for (startOffset = offset - 1; startOffset >= startLineOffset; startOffset -= 1)
420: if (!Character.isWhitespace(doc.getChar(startOffset)))
421: break;
422:
423: // move forward one (loop stopped after reaching too far)
424: startOffset += 1;
425:
426: // node ends on this line:
427: int endLine = doc.getLineOfOffset(offset + length);
428: // length of last line's delimiter:
429: int endLineDelimLength = doc.getLineDelimiter(endLine)
430: .length();
431: // hunt last whitespace/end of line with extraLength:
432: int extraLength = length;
433: while (true) {
434: extraLength += 1;
435: if (!Character.isWhitespace(doc.getChar(offset
436: + extraLength))) {
437: // found non-white space, move back one
438: extraLength -= 1;
439: break;
440: }
441: if (doc.getLineOfOffset(offset + extraLength) > endLine) {
442: // don't want to touch the lineDelimeters
443: extraLength -= endLineDelimLength;
444: break;
445: }
446: }
447:
448: // if we reached start of line, remove newline
449: if (startOffset == startLineOffset)
450: startOffset -= doc.getLineDelimiter(startLine).length();
451:
452: // add difference of new offset
453: length = extraLength + (offset - startOffset);
454: offset = startOffset;
455: // printDeletionRange(offset, length);
456: } catch (BadLocationException e) {
457: }
458: return new DeleteEdit(offset, length);
459: }
460:
461: protected void printDeletionRange(int offset, int length) {
462: try {
463: // newlines printed as \n
464: // carriage returns printed as \r
465: // tabs printed as \t
466: // spaces printed as *
467: String string = getDocumentProvider().getDocument(
468: getInput()).get(offset, length);
469: StringBuffer buffer = new StringBuffer();
470: for (int i = 0; i < string.length(); i++) {
471: char c = string.charAt(i);
472: if (c == '\n')
473: buffer.append("\\n"); //$NON-NLS-1$
474: else if (c == '\r')
475: buffer.append("\\r"); //$NON-NLS-1$
476: else if (c == '\t')
477: buffer.append("\\t"); //$NON-NLS-1$
478: else if (c == ' ')
479: buffer.append('*');
480: else
481: buffer.append(c);
482: }
483: System.out.println(buffer.toString());
484: } catch (BadLocationException e) {
485: }
486: }
487:
488: private IDocumentElementNode getHighestNodeToBeWritten(
489: IDocumentElementNode node) {
490: IDocumentElementNode parent = node.getParentNode();
491: if (parent == null)
492: return node;
493: if (parent.getOffset() > -1) {
494: IDocument doc = getDocumentProvider().getDocument(
495: getInput());
496: try {
497: String endChars = doc.get(parent.getOffset()
498: + parent.getLength() - 2, 2);
499: return ("/>".equals(endChars)) ? parent : node; //$NON-NLS-1$
500: } catch (BadLocationException e) {
501: return node;
502: }
503:
504: }
505: return getHighestNodeToBeWritten(parent);
506: }
507:
508: /* (non-Javadoc)
509: * @see org.eclipse.pde.internal.ui.neweditor.context.InputContext#flushModel(org.eclipse.jface.text.IDocument)
510: */
511: protected void flushModel(IDocument doc) {
512: removeUnnecessaryOperations();
513: if (fOperationTable.size() == 1) {
514: Object object = fOperationTable.keySet().iterator().next();
515: if (object instanceof IDocumentElementNode
516: && fEditOperations.get(0) instanceof InsertEdit) {
517: if (((IDocumentElementNode) object).getParentNode() == null) {
518: doc
519: .set(((IDocumentElementNode) object)
520: .write(true));
521: fOperationTable.clear();
522: fEditOperations.clear();
523: return;
524: }
525: }
526: }
527: reorderInsertEdits(fEditOperations);
528: fOperationTable.clear();
529: fMoveOperations.clear();
530: super .flushModel(doc);
531: }
532:
533: protected abstract void reorderInsertEdits(ArrayList ops);
534:
535: protected void removeUnnecessaryOperations() {
536: Iterator iter = fOperationTable.values().iterator();
537: while (iter.hasNext()) {
538: Object object = iter.next();
539: if (object instanceof IDocumentElementNode) {
540: IDocumentElementNode node = (IDocumentElementNode) object;
541: if (node.getOffset() > -1) {
542: IDocumentAttributeNode[] attrs = node
543: .getNodeAttributes();
544: for (int i = 0; i < attrs.length; i++) {
545: Object op = fOperationTable.remove(attrs[i]);
546: if (op != null)
547: fEditOperations.remove(op);
548: }
549: IDocumentTextNode textNode = node.getTextNode();
550: if (textNode != null) {
551: Object op = fOperationTable.remove(textNode);
552: if (op != null)
553: fEditOperations.remove(op);
554: }
555: }
556: }
557: }
558: }
559:
560: /**
561: * @param source
562: * @return
563: */
564: protected String getWritableAttributeNodeValue(String source) {
565: // TODO: MP: TEO: LOW: Shouldn't it be getWritableAttributeString ?
566: return PDEXMLHelper.getWritableString(source);
567: }
568:
569: /**
570: * @param source
571: * @return
572: */
573: protected String getWritableTextNodeString(
574: IDocumentTextNode textNode) {
575: return textNode.write();
576: }
577:
578: protected HashMap getOperationTable() {
579: return fOperationTable;
580: }
581:
582: }
|