add support for deleting forward (DEL key)

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteNextCharEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteNextCharEdit.java
new file mode 100644
index 0000000..e388eab
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/DeleteNextCharEdit.java
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Florian Thienel 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:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.undo;
+
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+
+/**
+ * @author Florian Thienel
+ */
+public class DeleteNextCharEdit extends AbstractUndoableEdit {
+
+	private final IDocument document;
+	private final int offset;
+
+	private int count;
+	private String textToRestore = null;
+
+	public DeleteNextCharEdit(final IDocument document, final int offset) {
+		this.document = document;
+		this.offset = offset;
+		count = 1;
+	}
+
+	@Override
+	protected boolean performCombine(final IUndoableEdit other) {
+		if (other instanceof DeleteNextCharEdit) {
+			final DeleteNextCharEdit otherEdit = (DeleteNextCharEdit) other;
+			if (otherEdit.offset == offset) {
+				count += otherEdit.count;
+				textToRestore += otherEdit.textToRestore;
+				return true;
+			}
+		}
+		return false;
+	}
+
+	@Override
+	protected void performRedo() throws CannotApplyException {
+		if (document.isTagAt(offset)) {
+			throw new CannotApplyException("Cannot delete a tag!");
+		}
+
+		try {
+			final ContentRange range = new ContentRange(offset, offset + count - 1);
+			textToRestore = document.getText(range);
+			document.delete(range);
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	@Override
+	protected void performUndo() throws CannotUndoException {
+		try {
+			document.insertText(offset, textToRestore);
+			textToRestore = null;
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	@Override
+	public int getOffsetBefore() {
+		return offset;
+	}
+
+	@Override
+	public int getOffsetAfter() {
+		return offset;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/JoinElementsAtOffsetEdit.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/JoinElementsAtOffsetEdit.java
new file mode 100644
index 0000000..787fe09
--- /dev/null
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/undo/JoinElementsAtOffsetEdit.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Florian Thienel 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:
+ * 		Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.undo;
+
+import org.eclipse.vex.core.provisional.dom.ContentRange;
+import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
+import org.eclipse.vex.core.provisional.dom.IDocument;
+import org.eclipse.vex.core.provisional.dom.IDocumentFragment;
+import org.eclipse.vex.core.provisional.dom.IElement;
+
+/**
+ * @author Florian Thienel
+ */
+public class JoinElementsAtOffsetEdit extends AbstractUndoableEdit {
+
+	private final IDocument document;
+	private final int offset;
+	private ContentRange rangeToRestore = null;
+	private IDocumentFragment fragmentToRestore = null;
+	private int offsetAfter;
+
+	public JoinElementsAtOffsetEdit(final IDocument document, final int offset) {
+		this.document = document;
+		this.offset = offset;
+		offsetAfter = offset;
+	}
+
+	@Override
+	protected void performRedo() throws CannotApplyException {
+		final IElement headElement;
+		final IElement tailElement;
+		if (isBetweenMatchingElements(document, offset - 1)) {
+			headElement = document.getElementForInsertionAt(offset - 2);
+			tailElement = document.getElementForInsertionAt(offset);
+		} else if (isBetweenMatchingElements(document, offset)) {
+			headElement = document.getElementForInsertionAt(offset - 1);
+			tailElement = document.getElementForInsertionAt(offset + 1);
+		} else if (isBetweenMatchingElements(document, offset + 1)) {
+			headElement = document.getElementForInsertionAt(offset);
+			tailElement = document.getElementForInsertionAt(offset + 2);
+		} else {
+			throw new CannotApplyException("The given offset " + offset + " is not between matching elements!");
+		}
+
+		final IDocumentFragment tailElementContent;
+		if (!tailElement.isEmpty()) {
+			tailElementContent = document.getFragment(tailElement.getRange().resizeBy(1, -1));
+		} else {
+			tailElementContent = null;
+		}
+
+		offsetAfter = headElement.getEndOffset();
+		fragmentToRestore = document.getFragment(headElement.getRange().union(tailElement.getRange()));
+
+		try {
+			document.delete(tailElement.getRange());
+			if (tailElementContent != null) {
+				document.insertFragment(headElement.getEndOffset(), tailElementContent);
+			}
+			rangeToRestore = headElement.getRange();
+		} catch (final DocumentValidationException e) {
+			throw new CannotApplyException(e);
+		}
+	}
+
+	public static boolean isBetweenMatchingElements(final IDocument document, final int offset) {
+		if (offset <= 1 || offset >= document.getLength() - 1) {
+			return false;
+		}
+		final IElement e1 = document.getElementForInsertionAt(offset - 1);
+		final IElement e2 = document.getElementForInsertionAt(offset + 1);
+		return e1 != e2 && e1 != null && e2 != null && e1.getParent() == e2.getParent() && e1.isKindOf(e2);
+	}
+
+	@Override
+	protected void performUndo() throws CannotUndoException {
+		try {
+			document.delete(rangeToRestore);
+			document.insertFragment(rangeToRestore.getStartOffset(), fragmentToRestore);
+			rangeToRestore = null;
+			fragmentToRestore = null;
+		} catch (final DocumentValidationException e) {
+			throw new CannotUndoException(e);
+		}
+	}
+
+	@Override
+	public int getOffsetBefore() {
+		return offset;
+	}
+
+	@Override
+	public int getOffsetAfter() {
+		return offsetAfter;
+	}
+
+}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java
index 0a7f67a..d267e23 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/swt/BoxWidget.java
@@ -43,6 +43,8 @@
 import org.eclipse.vex.core.internal.cursor.ICursorMove;
 import org.eclipse.vex.core.internal.cursor.ICursorPositionListener;
 import org.eclipse.vex.core.internal.undo.CannotApplyException;
+import org.eclipse.vex.core.internal.undo.DeleteEdit;
+import org.eclipse.vex.core.internal.undo.DeleteNextCharEdit;
 import org.eclipse.vex.core.internal.undo.EditStack;
 import org.eclipse.vex.core.internal.undo.IUndoableEdit;
 import org.eclipse.vex.core.internal.undo.InsertCommentEdit;
@@ -50,6 +52,7 @@
 import org.eclipse.vex.core.internal.undo.InsertLineBreakEdit;
 import org.eclipse.vex.core.internal.undo.InsertProcessingInstructionEdit;
 import org.eclipse.vex.core.internal.undo.InsertTextEdit;
+import org.eclipse.vex.core.internal.undo.JoinElementsAtOffsetEdit;
 import org.eclipse.vex.core.internal.visualization.IBoxModelBuilder;
 import org.eclipse.vex.core.internal.widget.BalancingSelector;
 import org.eclipse.vex.core.internal.widget.BoxView;
@@ -57,6 +60,7 @@
 import org.eclipse.vex.core.internal.widget.IViewPort;
 import org.eclipse.vex.core.internal.widget.ReadOnlyException;
 import org.eclipse.vex.core.internal.widget.VisualizationController;
+import org.eclipse.vex.core.provisional.dom.ContentRange;
 import org.eclipse.vex.core.provisional.dom.DocumentValidationException;
 import org.eclipse.vex.core.provisional.dom.IComment;
 import org.eclipse.vex.core.provisional.dom.IDocument;
@@ -211,6 +215,9 @@
 		case SWT.CR:
 			insertLineBreak();
 			break;
+		case SWT.DEL:
+			deleteForward();
+			break;
 		case 0x79:
 			if ((event.stateMask & SWT.CTRL) == SWT.CTRL) {
 				if (canRedo()) {
@@ -343,6 +350,36 @@
 		controller.moveCursor(toOffset(insertLineBreak.getOffsetAfter()));
 	}
 
+	public void deleteForward() {
+		final IUndoableEdit edit;
+		final int offset = cursor.getOffset();
+		if (offset == document.getLength()) {
+			// ignore
+			edit = null;
+		} else if (JoinElementsAtOffsetEdit.isBetweenMatchingElements(document, offset)) {
+			edit = new JoinElementsAtOffsetEdit(document, offset);
+		} else if (JoinElementsAtOffsetEdit.isBetweenMatchingElements(document, offset + 1)) {
+			edit = new JoinElementsAtOffsetEdit(document, offset);
+		} else if (document.getNodeForInsertionAt(offset).isEmpty()) {
+			final ContentRange range = document.getNodeForInsertionAt(offset).getRange();
+			edit = new DeleteEdit(document, range);
+		} else if (document.getNodeForInsertionAt(offset + 1).isEmpty()) {
+			final ContentRange range = document.getNodeForInsertionAt(offset + 1).getRange();
+			edit = new DeleteEdit(document, range);
+		} else if (!document.isTagAt(offset)) {
+			edit = new DeleteNextCharEdit(document, offset);
+		} else {
+			edit = null;
+		}
+
+		if (edit == null) {
+			return;
+		}
+
+		editStack.apply(edit);
+		controller.moveCursor(toOffset(edit.getOffsetAfter()));
+	}
+
 	public IElement insertElement(final QualifiedName elementName) throws DocumentValidationException {
 		final InsertElementEdit insertElement = editStack.apply(new InsertElementEdit(document, cursor.getOffset(), elementName));
 		final IElement element = insertElement.getElement();