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());