fix selection balancing according to the principle of least astonishment

Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SelectionTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SelectionTest.java
index 4a30fbb..64fa4bd 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SelectionTest.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/widget/L2SelectionTest.java
@@ -10,6 +10,7 @@
  *******************************************************************************/

 package org.eclipse.vex.core.internal.widget;

 

+import static org.eclipse.vex.core.internal.widget.VexWidgetTest.PARA;

 import static org.eclipse.vex.core.internal.widget.VexWidgetTest.TITLE;

 import static org.eclipse.vex.core.internal.widget.VexWidgetTest.createDocumentWithDTD;

 import static org.eclipse.vex.core.tests.TestResources.TEST_DTD;

@@ -84,4 +85,72 @@
 		assertEquals(titleElement.getRange(), widget.getSelectedRange());

 		assertEquals(titleElement.getEndOffset() + 1, widget.getCaretOffset());

 	}

+

+	@Test

+	public void givenCaretAtStartOffsetOfElementWithText_whenMovedOneForwardAndOneBackward_shouldSelectNothing() throws Exception {

+		final Element titleElement = widget.insertElement(TITLE);

+		widget.insertText("Hello World");

+		widget.moveTo(titleElement.getStartOffset(), false);

+		widget.moveBy(1, true);

+		widget.moveBy(-1, true);

+		assertEquals(titleElement.getStartOffset(), widget.getCaretOffset());

+	}

+

+	@Test

+	public void givenCaretInElementWithText_whenMovedBehindFollowingElementAndMovedBackOnce_shouldSelectOnlyFirstElement() throws Exception {

+		final Element titleElement = widget.insertElement(TITLE);

+		widget.insertText("Hello World");

+		widget.moveBy(1);

+		final Element paraElement = widget.insertElement(PARA);

+		widget.insertText("Hello Again");

+		widget.moveTo(titleElement.getStartOffset() + 3);

+		widget.moveTo(paraElement.getEndOffset() + 1, true);

+		widget.moveBy(-1, true);

+		assertEquals(titleElement.getRange(), widget.getSelectedRange());

+		assertEquals(titleElement.getEndOffset() + 1, widget.getCaretOffset());

+	}

+

+	@Test

+	public void givenCaretInElementWithText_whenMovedBehindFollowingElementAndMovedBackTwice_shouldSelectOnlyTextFragementOfFirstElement() throws Exception {

+		final Element titleElement = widget.insertElement(TITLE);

+		widget.insertText("Hello World");

+		widget.moveBy(1);

+		final Element paraElement = widget.insertElement(PARA);

+		widget.insertText("Hello Again");

+		widget.moveTo(titleElement.getStartOffset() + 3);

+		widget.moveTo(paraElement.getEndOffset() + 1, true);

+		widget.moveBy(-1, true);

+		widget.moveBy(-1, true);

+		assertEquals(titleElement.getRange().resizeBy(3, -1), widget.getSelectedRange());

+		assertEquals(titleElement.getEndOffset(), widget.getCaretOffset());

+	}

+

+	@Test

+	public void givenCaretInElementWithText_whenMovedBeforePrecedingElementAndMovedForwardOnce_shouldSelectOnlySecondElement() throws Exception {

+		final Element titleElement = widget.insertElement(TITLE);

+		widget.insertText("Hello World");

+		widget.moveBy(1);

+		final Element paraElement = widget.insertElement(PARA);

+		widget.insertText("Hello Again");

+		widget.moveTo(paraElement.getEndOffset() - 3);

+		widget.moveTo(titleElement.getStartOffset(), true);

+		widget.moveBy(1, true);

+		assertEquals(paraElement.getRange(), widget.getSelectedRange());

+		assertEquals(paraElement.getStartOffset(), widget.getCaretOffset());

+	}

+

+	@Test

+	public void givenCaretInElementWithText_whenMovedBeforePrecedingElementAndMovedForwardTwice_shouldSelectOnlyTextFragementOfSecondElement() throws Exception {

+		final Element titleElement = widget.insertElement(TITLE);

+		widget.insertText("Hello World");

+		widget.moveBy(1);

+		final Element paraElement = widget.insertElement(PARA);

+		widget.insertText("Hello Again");

+		widget.moveTo(paraElement.getEndOffset() - 3);

+		widget.moveTo(titleElement.getStartOffset(), true);

+		widget.moveBy(1, true);

+		widget.moveBy(1, true);

+		assertEquals(paraElement.getRange().resizeBy(1, -4), widget.getSelectedRange());

+		assertEquals(paraElement.getStartOffset() + 1, widget.getCaretOffset());

+	}

 }

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
index a3cb276..a5e7508 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
@@ -68,7 +68,7 @@
 		return children.size();

 	}

 

-	private ContentRange getInsertionRange() {

+	public ContentRange getInsertionRange() {

 		return getRange().resizeBy(1, 0);

 	}

 

diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/VexWidgetImpl.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/VexWidgetImpl.java
index 00fbe34..1c9313f 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/VexWidgetImpl.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/widget/VexWidgetImpl.java
@@ -843,42 +843,89 @@
 	}
 
 	public void moveTo(final int offset, final boolean select) {
-		if (offset < 1 || offset > document.getLength() - 1) {
+		if (!document.getInsertionRange().contains(offset)) {
 			return;
 		}
 
+		final boolean movingForward = offset > caretOffset;
+		final boolean movingBackward = offset < caretOffset;
+		final boolean movingTowardMark = movingForward && mark >= offset || movingBackward && mark <= offset;
+		final boolean movingAwayFromMark = !movingTowardMark;
+
 		repaintCaret();
 		repaintRange(getSelectionStart(), getSelectionEnd());
 
 		final Element oldElement = currentElement;
 
 		caretOffset = offset;
-
-		currentElement = document.getElementForInsertionAt(offset);
+		currentElement = document.getElementForInsertionAt(caretOffset);
 
 		if (select) {
 			selectionStart = Math.min(mark, caretOffset);
 			selectionEnd = Math.max(mark, caretOffset);
 
-			// move selectionStart and selectionEnd to make sure we don't select a partial element
+			// expand or shrink the selection to make sure the selection is balanced
 			final Element commonElement = document.findCommonElement(selectionStart, selectionEnd);
-
-			Element element = document.getElementForInsertionAt(selectionStart);
-			while (element != commonElement) {
-				selectionStart = element.getStartOffset();
-				if (mark > caretOffset) {
+			if (movingForward && movingTowardMark) {
+				// shrink start
+				Element element = document.getElementForInsertionAt(selectionStart);
+				while (element != commonElement) {
+					selectionStart = element.getEndOffset() + 1;
 					caretOffset = selectionStart;
+					element = document.getElementForInsertionAt(selectionStart);
 				}
-				element = document.getElementForInsertionAt(selectionStart);
-			}
 
-			element = document.getElementForInsertionAt(selectionEnd);
-			while (element != commonElement) {
-				selectionEnd = element.getEndOffset() + 1;
-				if (mark < caretOffset) {
-					caretOffset = selectionEnd;
-				}
+				// expand end
 				element = document.getElementForInsertionAt(selectionEnd);
+				while (element != commonElement) {
+					selectionEnd = element.getEndOffset() + 1;
+					element = document.getElementForInsertionAt(selectionEnd);
+				}
+			} else if (movingBackward && movingTowardMark) {
+				// shrink end
+				Element element = document.getElementForInsertionAt(selectionEnd);
+				while (element != commonElement) {
+					selectionEnd = element.getStartOffset();
+					caretOffset = selectionEnd;
+					element = document.getElementForInsertionAt(selectionEnd);
+				}
+
+				// expand start
+				element = document.getElementForInsertionAt(selectionStart);
+				while (element != commonElement) {
+					selectionStart = element.getStartOffset();
+					element = document.getElementForInsertionAt(selectionStart);
+				}
+			} else if (movingForward && movingAwayFromMark) {
+				// expand end
+				Element element = document.getElementForInsertionAt(selectionEnd);
+				while (element != commonElement) {
+					selectionEnd = element.getEndOffset() + 1;
+					caretOffset = selectionEnd;
+					element = document.getElementForInsertionAt(selectionEnd);
+				}
+
+				// expand start
+				element = document.getElementForInsertionAt(selectionStart);
+				while (element != commonElement) {
+					selectionStart = element.getStartOffset();
+					element = document.getElementForInsertionAt(selectionStart);
+				}
+			} else if (movingBackward && movingAwayFromMark) {
+				// expand start
+				Element element = document.getElementForInsertionAt(selectionStart);
+				while (element != commonElement) {
+					selectionStart = element.getStartOffset();
+					caretOffset = selectionStart;
+					element = document.getElementForInsertionAt(selectionStart);
+				}
+
+				// expand end
+				element = document.getElementForInsertionAt(selectionEnd);
+				while (element != commonElement) {
+					selectionEnd = element.getEndOffset() + 1;
+					element = document.getElementForInsertionAt(selectionEnd);
+				}
 			}
 
 		} else {
@@ -914,11 +961,8 @@
 		g.dispose();
 
 		magicX = -1;
-
 		scrollCaretVisible();
-
 		hostComponent.fireSelectionChanged();
-
 		caretVisible = true;
 
 		repaintRange(getSelectionStart(), getSelectionEnd());