Bug 576377 - Provide shortcuts/commands for incremental
multiselection/multiple carets in text editors

Expansion of the multi-selection commands to consider the first
selection range as an anchor to which then subsequent command calls
relate. Thus it's now also possible to create a multi-selection
incrementally "upwards" from the initial anchor selection, and
also revert downwards.

Change-Id: Ica3e444064df9373fee65bd5f4b4bcf2cb146750
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.text/+/191824
Tested-by: Mickael Istria <mistria@redhat.com>
Reviewed-by: Mickael Istria <mistria@redhat.com>
diff --git a/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextMultiCaretSelectionCommandsTest.java b/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextMultiCaretSelectionCommandsTest.java
index 4ddeb9f..3165209 100644
--- a/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextMultiCaretSelectionCommandsTest.java
+++ b/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextMultiCaretSelectionCommandsTest.java
@@ -52,9 +52,9 @@
  * an editor from this bundle is quite tricky without the IDE and EFS utils.
  */
 public class TextMultiCaretSelectionCommandsTest {
-	private static final String ADD_NEXT_MATCH_TO_MULTI_SELECTION = "org.eclipse.ui.edit.text.select.addNextMatchToMultiSelection";
+	private static final String MULTI_SELECTION_DOWN = "org.eclipse.ui.edit.text.select.selectMultiSelectionDown";
 	private static final String ADD_ALL_MATCHES_TO_MULTI_SELECTION = "org.eclipse.ui.edit.text.select.addAllMatchesToMultiSelection";
-	private static final String REMOVE_LAST_MATCH_FROM_MULTI_SELECTION = "org.eclipse.ui.edit.text.select.removeLastMatchFromMultiSelection";
+	private static final String MULTI_SELECTION_UP = "org.eclipse.ui.edit.text.select.selectMultiSelectionUp";
 	private static final String STOP_MULTI_SELECTION = "org.eclipse.ui.edit.text.select.stopMultiSelection";
 
 	private static final String LINE_1 = "private static String a;\n";
@@ -88,16 +88,17 @@
 	}
 
 	@Test
-	public void testAddNextMatch_withFirstIdentifierSelected_addsIdenticalIdentifiersToSelection() throws Exception {
+	public void testMultiSelectionDown_withFirstIdentifierSelected_addsIdenticalIdentifiersToSelection()
+			throws Exception {
 		setSelection(new IRegion[] { new Region(0, 7) });
 		assertEquals(7, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(7, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7) }, getSelection());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(7, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN, 7) },
@@ -109,24 +110,24 @@
 	}
 
 	@Test
-	public void testAddNextMatch_withSecondIdentifierSelectedIdentifier_addsNextOccurenceToSelection()
+	public void testMultiSelectionDown_withSecondIdentifierSelectedIdentifier_addsNextOccurenceToSelection()
 			throws Exception {
 		setSelection(new IRegion[] { new Region(8, 6) });
 		assertEquals(14, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(14, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6) }, getSelection());
 	}
 
 	@Test
-	public void testAddNextMatch_withSelectionInSecondRow_addsIdenticalIdentifierInThirdRowToSelection()
+	public void testMultiSelectionDown_withSelectionInSecondRow_addsIdenticalIdentifierInThirdRowToSelection()
 			throws Exception {
 		setSelection(new IRegion[] { new Region(L1_LEN + 8, 6) });
 		assertEquals(L1_LEN + 14, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(L1_LEN + 14, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 8, 6) },
@@ -134,67 +135,99 @@
 	}
 
 	@Test
-	public void testAddNextMatch_withCaretInFirstIdentifier_selectsFullIdentifier() throws Exception {
+	public void testMultiSelectionDown_withTwoSelectionsAndAnchorBelow_reducesSelection() throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN + 8, 6) });
+		// It is important here to build up the selection in steps, so the
+		// handler can determine an anchor region
+		executeCommand(MULTI_SELECTION_UP);
+		assertArrayEquals(new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6) }, getSelection());
+
+		executeCommand(MULTI_SELECTION_DOWN);
+
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6) }, getSelection());
+	}
+
+	@Test
+	public void testMultiSelectionDown_withTwoSelectionsAndAnchorAbove_extendsSelection() throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN + 8, 6) });
+		// It is important here to build up the selection in steps, so the
+		// handler can determine an anchor region
+		executeCommand(MULTI_SELECTION_DOWN);
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 8, 6) },
+				getSelection());
+
+		executeCommand(MULTI_SELECTION_DOWN);
+
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 8, 6),
+				new Region(L1_LEN + L2_LEN + L3_LEN + 8, 6) }, getSelection());
+	}
+
+	// Caret-related tests for ADD_NEXT_MATCH_TO_MULTI_SELECTION
+	// that check how the selection is expanded
+
+	@Test
+	public void testMultiSelectionDown_withCaretInFirstIdentifier_selectsFullIdentifier() throws Exception {
 		setSelection(new IRegion[] { new Region(1, 0) });
 		assertEquals(1, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(7, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(0, 7) }, getSelection());
 	}
 
 	@Test
-	public void testAddNextMatch_withCaretInSecondIdentifier_selectsFullIdentifier() throws Exception {
+	public void testMultiSelectionDown_withCaretInSecondIdentifier_selectsFullIdentifier() throws Exception {
 		setSelection(new IRegion[] { new Region(11, 0) });
 		assertEquals(11, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(14, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(8, 6) }, getSelection());
 	}
 
 	@Test
-	public void testAddNextMatch_withCaretBetweenIdentifierCharAndNonIdentifierChar_selectsFullIdentifier()
+	public void testMultiSelectionDown_withCaretBetweenIdentifierCharAndNonIdentifierChar_selectsFullIdentifier()
 			throws Exception {
 		setSelection(new IRegion[] { new Region(23, 0) });
 		assertEquals(23, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(23, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(22, 1) }, getSelection());
 	}
 
 	@Test
-	public void testAddNextMatch_withCaretInSecondRow_selectsFullIdentifier() throws Exception {
+	public void testMultiSelectionDown_withCaretInSecondRow_selectsFullIdentifier() throws Exception {
 		setSelection(new IRegion[] { new Region(L1_LEN + 11, 0) });
 		assertEquals(L1_LEN + 11, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(L1_LEN + 14, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6) }, getSelection());
 	}
 
 	@Test
-	public void testAddNextMatch_withCaretInIdentifierWithNoFollowingMatch_selectsFullIdentifier() throws Exception {
+	public void testMultiSelectionDown_withCaretInIdentifierWithNoFollowingMatch_selectsFullIdentifier()
+			throws Exception {
 		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN + L3_LEN + 11, 0) });
 		assertEquals(L1_LEN + L2_LEN + L3_LEN + 11, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(L1_LEN + L2_LEN + L3_LEN + 14, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(L1_LEN + L2_LEN + L3_LEN + 8, 6) }, getSelection());
 	}
 
 	@Test
-	public void testAddNextMatch_withCaretAtEndOfDocument_selectsFullIdentifier() throws Exception {
+	public void testMultiSelectionDown_withCaretAtEndOfDocument_selectsFullIdentifier() throws Exception {
 		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN + L3_LEN + L4_LEN, 0) });
 		assertEquals(L1_LEN + L2_LEN + L3_LEN + L4_LEN, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		assertEquals(L1_LEN + L2_LEN + L3_LEN + L4_LEN, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(L1_LEN + L2_LEN + L3_LEN + L4_LEN - 1, 1) }, getSelection());
@@ -248,50 +281,82 @@
 	}
 
 	@Test
-	public void testRemoveLastMatchFromMultiSelection_withCaretInIdentifier_doesNothing() throws Exception {
+	public void testMultiSelectionUp_withCaretInIdentifier_selectsFullIdentifier() throws Exception {
 		setSelection(new IRegion[] { new Region(L1_LEN + 11, 0) });
 		assertEquals(L1_LEN + 11, widget.getCaretOffset());
 
-		executeCommand(REMOVE_LAST_MATCH_FROM_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_UP);
 
-		assertEquals(L1_LEN + 11, widget.getCaretOffset());
-		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 11, 0) }, getSelection());
+		assertEquals(L1_LEN + 8 + 6, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6) }, getSelection());
 	}
 
 	@Test
-	public void testRemoveLastMatchFromMultiSelection_withSingleSelection_doesNothing() throws Exception {
+	public void testMultiSelectionUp_withSingleSelectionAndNoPreviousMatch_doesNothing()
+			throws Exception {
 		setSelection(new IRegion[] { new Region(8, 6) });
 		assertEquals(14, widget.getCaretOffset());
 
-		executeCommand(REMOVE_LAST_MATCH_FROM_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_UP);
 
 		assertEquals(14, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(8, 6) }, getSelection());
 	}
 
 	@Test
-	public void testRemoveLastMatchFromMultiSelection_withTwoSelections_removesSecondSelection() throws Exception {
+	public void testMultiSelectionUp_withTwoSelections_removesSecondSelection() throws Exception {
 		setSelection(new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6) });
 		assertEquals(14, widget.getCaretOffset());
 
-		executeCommand(REMOVE_LAST_MATCH_FROM_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_UP);
 
 		assertEquals(14, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(8, 6) }, getSelection());
 	}
 
 	@Test
-	public void testRemoveLastMatchFromMultiSelection_withThreeSelections_removesThirdSelection() throws Exception {
+	public void testMultiSelectionUp_withThreeSelections_removesThirdSelection() throws Exception {
 		setSelection(new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 8, 6) });
 		assertEquals(14, widget.getCaretOffset());
 
-		executeCommand(REMOVE_LAST_MATCH_FROM_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_UP);
 
 		assertEquals(14, widget.getCaretOffset());
 		assertArrayEquals(new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6) }, getSelection());
 	}
 
 	@Test
+	public void testMultiSelectionUp_withTwoSelectionsAndAnchorAbove_reducesSelection()
+			throws Exception {
+		setSelection(new IRegion[] { new Region(8, 6) });
+		// It is important here to build up the selection in steps, so the
+		// handler can determine an anchor region
+		executeCommand(MULTI_SELECTION_DOWN);
+		assertArrayEquals(new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6) }, getSelection());
+
+		executeCommand(MULTI_SELECTION_UP);
+
+		assertArrayEquals(new IRegion[] { new Region(8, 6) }, getSelection());
+	}
+
+	@Test
+	public void testMultiSelectionUp_withTwoSelectionsAndAnchorBelow_extendsSelection()
+			throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN + 8, 6) });
+		// It is important here to build up the selection in steps, so the
+		// handler can determine an anchor region
+		executeCommand(MULTI_SELECTION_UP);
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 8, 6) },
+				getSelection());
+
+		executeCommand(MULTI_SELECTION_UP);
+
+		assertArrayEquals(
+				new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 8, 6) },
+				getSelection());
+	}
+
+	@Test
 	public void testStopMultiSelection_withSingleSelection_doesNotChangeSelectionOrCaretOffset() throws Exception {
 		setSelection(new IRegion[] { new Region(0, 7) });
 
@@ -330,8 +395,8 @@
 		setSelection(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN, 7) });
 		assertEquals(7, widget.getCaretOffset());
 
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
-		executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION);
+		executeCommand(MULTI_SELECTION_DOWN);
+		executeCommand(MULTI_SELECTION_DOWN);
 
 		// TODO How to place the caret at the end without dismissing the
 		// selection? Should rather be 57
diff --git a/org.eclipse.ui.workbench.texteditor/plugin.properties b/org.eclipse.ui.workbench.texteditor/plugin.properties
index f7bf561..2e204f5 100644
--- a/org.eclipse.ui.workbench.texteditor/plugin.properties
+++ b/org.eclipse.ui.workbench.texteditor/plugin.properties
@@ -161,10 +161,10 @@
 command.selectWindowStart.name = Select Window Start
 command.selectAddAllMatchesToMultiSelection.description = Looks for all regions matching the current selection or identifier and adds them to a multi-selection 
 command.selectAddAllMatchesToMultiSelection.name = Add all matches to multi-selection
-command.selectAddNextMatchToMultiSelection.description = Looks for the next region matching the current selection and adds it to a multi-selection 
-command.selectAddNextMatchToMultiSelection.name = Add next match to multi-selection
-command.selectRemoveLastMatchFromMultiSelection.description = Reduces the current matching regions of a multi-selection by one 
-command.selectRemoveLastMatchFromMultiSelection.name = Remove last match from multi-selection
+command.selectMultiSelectionDown.description = Search next matching region and add it to the current selection, or remove first element from current multi-selection 
+command.selectMultiSelectionDown.name = Multi selection down relative to anchor selection  
+command.selectMultiSelectionUp.description = Search next matching region above and add it to the current selection, or remove last element from current multi-selection 
+command.selectMultiSelectionUp.name = Multi selection up relative to anchor selection
 command.stopMultiSelection.description = Unselects all multi-selections returning to a single cursor 
 command.stopMultiSelection.name = End multi-selection
 command.selectWordNext.description = Select the next word
diff --git a/org.eclipse.ui.workbench.texteditor/plugin.xml b/org.eclipse.ui.workbench.texteditor/plugin.xml
index f8d54ff..70d47cf 100644
--- a/org.eclipse.ui.workbench.texteditor/plugin.xml
+++ b/org.eclipse.ui.workbench.texteditor/plugin.xml
@@ -311,16 +311,16 @@
 	        id="org.eclipse.ui.edit.text.select.addAllMatchesToMultiSelection">
 	  </command>
 	  <command
-	        name="%command.selectAddNextMatchToMultiSelection.name"
-	        description="%command.selectAddNextMatchToMultiSelection.description"
+	        name="%command.selectMultiSelectionDown.name"
+	        description="%command.selectMultiSelectionDown.description"
 	        categoryId="org.eclipse.ui.category.textEditor"
-	        id="org.eclipse.ui.edit.text.select.addNextMatchToMultiSelection">
+	        id="org.eclipse.ui.edit.text.select.selectMultiSelectionDown">
 	  </command>
 	  <command
-	        name="%command.selectRemoveLastMatchFromMultiSelection.name"
-	        description="%command.selectAddNextMatchToMultiSelection.description"
+	        name="%command.selectMultiSelectionUp.name"
+	        description="%command.selectMultiSelectionUp.description"
 	        categoryId="org.eclipse.ui.category.textEditor"
-	        id="org.eclipse.ui.edit.text.select.removeLastMatchFromMultiSelection">
+	        id="org.eclipse.ui.edit.text.select.selectMultiSelectionUp">
 	  </command>
 	  <command
 	        name="%command.stopMultiSelection.name"
@@ -1402,8 +1402,8 @@
          </enabledWhen>
       </handler>	
       <handler
-            class="org.eclipse.ui.internal.texteditor.multiselection.AddNextMatchToMultiSelectionHandler"
-            commandId="org.eclipse.ui.edit.text.select.addNextMatchToMultiSelection">
+            class="org.eclipse.ui.internal.texteditor.multiselection.MultiSelectionDownHandler"
+            commandId="org.eclipse.ui.edit.text.select.selectMultiSelectionDown">
          <enabledWhen>
             <with
                  variable="activeEditor">
@@ -1414,8 +1414,8 @@
          </enabledWhen>
       </handler>
       <handler
-            class="org.eclipse.ui.internal.texteditor.multiselection.RemoveLastMatchFromMultiSelectionHandler"
-            commandId="org.eclipse.ui.edit.text.select.removeLastMatchFromMultiSelection">
+            class="org.eclipse.ui.internal.texteditor.multiselection.MultiSelectionUpHandler"
+            commandId="org.eclipse.ui.edit.text.select.selectMultiSelectionUp">
          <enabledWhen>
             <with
                  variable="activeEditor">
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AbstractMultiSelectionHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AbstractMultiSelectionHandler.java
index f54d153..ba42de9 100644
--- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AbstractMultiSelectionHandler.java
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AbstractMultiSelectionHandler.java
@@ -46,11 +46,16 @@
  * initialized.
  *
  * @see AddAllMatchesToMultiSelectionHandler
- * @see AddNextMatchToMultiSelectionHandler
- * @see RemoveLastMatchFromMultiSelectionHandler
+ * @see MultiSelectionDownHandler
+ * @see MultiSelectionUpHandler
  * @see StopMultiSelectionHandler
  */
 abstract class AbstractMultiSelectionHandler extends AbstractHandler {
+	/**
+	 * Each widget can have a different anchor selection, that is stored in the
+	 * widget's data with this key.
+	 */
+	private static final String ANCHOR_REGION_KEY = "org.eclipse.ui.internal.texteditor.multiselection.AbstractMultiSelectionHandler.anchorRegion"; //$NON-NLS-1$
 	private ExecutionEvent event;
 	private ITextEditor textEditor;
 	private IDocument document;
@@ -58,7 +63,7 @@
 	/**
 	 * This method needs to be overwritten from subclasses to handle the event.
 	 *
-	 * @throws ExecutionException
+	 * @throws ExecutionException an Exception the event handler might throw
 	 */
 	public abstract void execute() throws ExecutionException;
 
@@ -81,7 +86,7 @@
 
 	protected boolean nothingSelected() {
 		IRegion[] regions = getSelectedRegions();
-		return regions == null || regions.length == 0 || regions[0].getLength() == 0;
+		return regions == null || regions.length == 0 || (regions.length == 1 && regions[0].getLength() == 0);
 	}
 
 	protected IRegion[] getSelectedRegions() {
@@ -95,34 +100,74 @@
 	}
 
 	protected IRegion offsetAsCaretRegion(int offset) {
-		return new Region(offset, 0);
+		return createRegionIfValid(offset, 0);
 	}
 
-	protected void selectRegion(IRegion region) throws ExecutionException {
+	protected void selectRegion(IRegion region) {
 		selectRegions(new IRegion[] { region });
 	}
 
-	protected void selectRegions(IRegion[] regions) throws ExecutionException {
+	protected void selectRegions(IRegion[] regions) {
 		setBlockSelectionMode(false);
 
 		ISelection newSelection = new MultiTextSelection(document, regions);
 		textEditor.getSelectionProvider().setSelection(newSelection);
 	}
 
-	protected void selectIdentifierUnderCaret() throws ExecutionException {
+	protected void selectIdentifierUnderCaret() {
 		int offset = getCaretOffset();
 
 		Region identifierRegion = getIdentifierUnderCaretRegion(offset);
-		if (identifierRegion != null)
+		if (identifierRegion != null) {
 			selectRegion(identifierRegion);
+			setAnchorRegion(identifierRegion);
+		}
+	}
+
+	protected void selectCaretPosition() {
+		IRegion caretRegion = offsetAsCaretRegion(getCaretOffset());
+		selectRegion(caretRegion);
+		setAnchorRegion(caretRegion);
 	}
 
 	protected boolean allRegionsHaveSameText() {
-		if (nothingSelected())
-			return false;
 		return allRegionsHaveSameText(getSelectedRegions());
 	}
 
+	protected boolean allRegionsEmpty() {
+		IRegion[] selectedRegions = getSelectedRegions();
+		if (selectedRegions == null)
+			return true;
+		return isEmpty(selectedRegions[0]) && allRegionsHaveSameText(selectedRegions);
+	}
+
+	protected boolean isEmpty(IRegion region) {
+		return region == null || region.getLength() == 0;
+	}
+
+	protected IRegion getAnchorRegion() {
+		return (IRegion) getWidget().getData(ANCHOR_REGION_KEY);
+	}
+
+	protected void setAnchorRegion(IRegion selection) {
+		if (selection == null) {
+			getWidget().setData(ANCHOR_REGION_KEY, null);
+		} else {
+			getWidget().setData(ANCHOR_REGION_KEY, selection);
+		}
+	}
+
+	private void initAnchorRegion() {
+		IRegion[] regions = getSelectedRegions();
+		if ((regions != null && regions.length == 1) || !contains(regions, getAnchorRegion())) {
+			setAnchorRegion(regions[0]);
+		}
+	}
+
+	private boolean contains(IRegion[] regions, IRegion region) {
+		return Arrays.asList(regions).contains(region);
+	}
+
 	private boolean allRegionsHaveSameText(IRegion[] regions) {
 		if (regions == null || regions.length == 1)
 			return true;
@@ -163,6 +208,16 @@
 		return Arrays.copyOf(regions, regions.length - 1);
 	}
 
+	protected IRegion[] removeFirstRegionButOne(IRegion[] regions) {
+		if (regions == null || regions.length == 0)
+			return null;
+		if (regions.length == 1) {
+			return regions;
+		}
+
+		return Arrays.copyOfRange(regions, 1, regions.length);
+	}
+
 	protected int getCaretOffset() {
 		return getWidget().getCaretOffset();
 	}
@@ -172,24 +227,49 @@
 	}
 
 	protected IRegion findNextMatch(IRegion region) throws ExecutionException {
-		String fullText = getFullText();
 		try {
-			String searchString = getTextOfRegion(region);
+			if (region.getLength() == 0) {
+				return offsetAsCaretRegion(offsetInNextLine(region.getOffset()));
+			} else {
+				String searchString = getTextOfRegion(region);
 
-			int matchPos = fullText.indexOf(searchString, offsetAfter(region));
-			if (matchPos < 0)
-				return null;
-
-			return new Region(matchPos, region.getLength());
+				String fullText = getFullText();
+				int matchPos = fullText.indexOf(searchString, offsetAfter(region));
+				return createRegionIfValid(matchPos, region.getLength());
+			}
 		} catch (BadLocationException e) {
 			throw new ExecutionException("Internal error in findNextMatch", e);
 		}
 	}
 
+	protected IRegion findPreviousMatch(IRegion region) throws ExecutionException {
+		try {
+			if (region.getLength() == 0) {
+				return offsetAsCaretRegion(offsetInPreviousLine(region.getOffset()));
+			} else {
+				String searchString = getTextOfRegion(region);
+
+				String fullText = getFullText();
+				int matchPos = fullText.lastIndexOf(searchString, region.getOffset() - 1);
+				return createRegionIfValid(matchPos, region.getLength());
+			}
+		} catch (BadLocationException e) {
+			throw new ExecutionException("Internal error in findPreviousMatch", e);
+		}
+	}
+
+	private IRegion createRegionIfValid(int offset, int length) {
+		if ((offset < 0) || (offset > document.getLength()))
+			return null;
+
+		return new Region(offset, Math.min(length, document.getLength() - offset));
+	}
+
 	protected IRegion[] findAllMatches(IRegion region) throws ExecutionException {
 		try {
-			String fullText = getFullText();
 			String searchString = getTextOfRegion(region);
+
+			String fullText = getFullText();
 			List<IRegion> regions = findAllMatches(fullText, searchString);
 			return toArray(regions);
 		} catch (BadLocationException e) {
@@ -208,18 +288,39 @@
 		return regions;
 	}
 
+	private int offsetInNextLine(int offset) throws BadLocationException {
+		return moveOffsetByLines(offset, 1);
+	}
+
+	private int offsetInPreviousLine(int offset) throws BadLocationException {
+		return moveOffsetByLines(offset, -1);
+	}
+
+	private int moveOffsetByLines(int offset, int lineDelta) throws BadLocationException {
+		int lineNo = document.getLineOfOffset(offset);
+		int newLineNo = lineNo + lineDelta;
+		if ((newLineNo < 0) || (newLineNo >= document.getNumberOfLines()))
+			return -1;
+
+		int newLineOffset = document.getLineOffset(newLineNo);
+		int delta = offset - document.getLineOffset(lineNo);
+
+		return newLineOffset + delta;
+	}
+
 	private boolean initFrom(ExecutionEvent event) {
 		this.event = event;
-		textEditor = getTextEditor(event);
+		initTextEditor();
 		if (textEditor == null)
 			return false;
 		document = getDocument();
+		initAnchorRegion();
 		return true;
 	}
 
-	private ITextEditor getTextEditor(ExecutionEvent event) {
+	private void initTextEditor() {
 		IEditorPart editor = HandlerUtil.getActiveEditor(event);
-		return editor instanceof ITextEditor ? (ITextEditor) editor : null;
+		textEditor = editor instanceof ITextEditor ? (ITextEditor) editor : null;
 	}
 
 	private IDocument getDocument() {
@@ -291,4 +392,32 @@
 		ITextEditorExtension5 ext = (ITextEditorExtension5) textEditor;
 		ext.setBlockSelectionMode(blockSelectionMode);
 	}
+
+	protected boolean selectionIsAboveAnchorRegion() {
+		IRegion[] selectedRegions = getSelectedRegions();
+		if (selectedRegions == null || selectedRegions.length == 1)
+			return false;
+		return isLastRegion(getAnchorRegion(), selectedRegions);
+	}
+
+	protected boolean selectionIsBelowAnchorRegion() {
+		IRegion[] selectedRegions = getSelectedRegions();
+		if (selectedRegions == null || selectedRegions.length == 1)
+			return false;
+		return isFirstRegion(getAnchorRegion(), selectedRegions);
+	}
+
+	private boolean isLastRegion(IRegion region, IRegion[] regions) {
+		if (region == null || regions == null || regions.length == 0)
+			return false;
+
+		return region.equals(regions[regions.length - 1]);
+	}
+
+	private boolean isFirstRegion(IRegion region, IRegion[] regions) {
+		if (region == null || regions == null || regions.length == 0)
+			return false;
+
+		return region.equals(regions[0]);
+	}
 }
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddAllMatchesToMultiSelectionHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddAllMatchesToMultiSelectionHandler.java
index b9d09fc..189ea10 100644
--- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddAllMatchesToMultiSelectionHandler.java
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddAllMatchesToMultiSelectionHandler.java
@@ -11,12 +11,10 @@
  * Contributors:
  *     Dirk Steinkamp <dirk.steinkamp@gmx.de> - initial API and implementation
  *******************************************************************************/
- package org.eclipse.ui.internal.texteditor.multiselection;
+package org.eclipse.ui.internal.texteditor.multiselection;
 
 import org.eclipse.core.commands.ExecutionException;
 
-import org.eclipse.jface.text.IRegion;
-
 /**
  * Handler to extend the current selection to all found matches in the document.
  * If nothing is selected, an implicit selection of the word under the cursor is
@@ -34,8 +32,9 @@
 
 	private void extendSelectionToAllMatches() throws ExecutionException {
 		if (allRegionsHaveSameText()) {
-			IRegion[] regions = getSelectedRegions();
-			selectRegions(findAllMatches(regions[0]));
+			if (!isEmpty(getAnchorRegion())) {
+				selectRegions(findAllMatches(getAnchorRegion()));
+			}
 		}
 	}
 }
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddNextMatchToMultiSelectionHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddNextMatchToMultiSelectionHandler.java
deleted file mode 100644
index 16f9d47..0000000
--- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddNextMatchToMultiSelectionHandler.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2022 Dirk Steinkamp
- *
- * This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License 2.0
- * which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Contributors:
- *     Dirk Steinkamp <dirk.steinkamp@gmx.de> - initial API and implementation
- *******************************************************************************/
-package org.eclipse.ui.internal.texteditor.multiselection;
-
-import org.eclipse.core.commands.ExecutionException;
-
-import org.eclipse.jface.text.IRegion;
-
-/**
- * Handler to extend the current selection to the next found match below the
- * selection. If no word is selected, an implicit selection of the word under
- * the cursor is performed.
- */
-public class AddNextMatchToMultiSelectionHandler extends AbstractMultiSelectionHandler {
-
-	@Override
-	public void execute() throws ExecutionException {
-		if (nothingSelected()) {
-			selectIdentifierUnderCaret();
-		} else {
-			extendSelectionToNextMatch();
-		}
-	}
-
-	private void extendSelectionToNextMatch() throws ExecutionException {
-		if (allRegionsHaveSameText()) {
-			IRegion[] regions = getSelectedRegions();
-			IRegion nextMatch = findNextMatch(regions[regions.length - 1]);
-			selectRegions(addRegion(regions, nextMatch));
-		}
-	}
-}
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiSelectionDownHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiSelectionDownHandler.java
new file mode 100644
index 0000000..678077e
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiSelectionDownHandler.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) 2022 Dirk Steinkamp
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Dirk Steinkamp <dirk.steinkamp@gmx.de> - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.ui.internal.texteditor.multiselection;
+
+import org.eclipse.core.commands.ExecutionException;
+
+import org.eclipse.jface.text.IRegion;
+
+/**
+ * Handler to change the current multi selection downwards. This might either
+ * mean to extend the selection by adding a match below, or shrink the selection
+ * by removing the first selection range. This depends on the selection the
+ * command was invoked with the first time -- that selection is remembered as an
+ * "anchor" to which successive calls are related as reference selection.<br>
+ * If no word is selected, an implicit selection of the word under the cursor is
+ * performed.
+ */
+public class MultiSelectionDownHandler extends AbstractMultiSelectionHandler {
+
+	@Override
+	public void execute() throws ExecutionException {
+		if (nothingSelected()) {
+			selectIdentifierUnderCaret();
+		} else if (selectionIsAboveAnchorRegion()) {
+			removeFirstRegionFromSelection();
+		} else {
+			extendSelectionToNextMatch();
+		}
+	}
+
+	private void extendSelectionToNextMatch() throws ExecutionException {
+		if (allRegionsHaveSameText()) {
+			IRegion[] regions = getSelectedRegions();
+			IRegion nextMatch = findNextMatch(regions[regions.length - 1]);
+			selectRegions(addRegion(regions, nextMatch));
+		}
+	}
+
+	private void removeFirstRegionFromSelection() {
+		if (allRegionsHaveSameText()) {
+			selectRegions(removeFirstRegionButOne(getSelectedRegions()));
+		}
+	}
+}
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiSelectionUpHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiSelectionUpHandler.java
new file mode 100644
index 0000000..8205b3a
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiSelectionUpHandler.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) 2022 Dirk Steinkamp
+ *
+ * This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License 2.0
+ * which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ *     Dirk Steinkamp <dirk.steinkamp@gmx.de> - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.ui.internal.texteditor.multiselection;
+
+import org.eclipse.core.commands.ExecutionException;
+
+import org.eclipse.jface.text.IRegion;
+
+/**
+ * Handler to change the current multi selection upwards. This might either mean
+ * to extend the selection by adding a match above, or shrink the selection by
+ * removing the last selection range. This depends on the selection the command
+ * was invoked with the first time -- that selection is remembered as an
+ * "anchor" to which successive calls are related as reference selection.<br>
+ * If no word is selected, an implicit selection of the word under the cursor is
+ * performed.
+ */
+public class MultiSelectionUpHandler extends AbstractMultiSelectionHandler {
+
+	@Override
+	public void execute() throws ExecutionException {
+		if (nothingSelected()) {
+			selectIdentifierUnderCaret();
+		} else if (selectionIsBelowAnchorRegion()) {
+			removeLastRegionFromSelection();
+		} else {
+			extendSelectionToPreviousMatch();
+		}
+	}
+
+	private void extendSelectionToPreviousMatch() throws ExecutionException {
+		if (allRegionsHaveSameText()) {
+			IRegion[] regions = getSelectedRegions();
+			IRegion nextMatch = findPreviousMatch(regions[0]);
+			selectRegions(addRegion(regions, nextMatch));
+		}
+	}
+
+	private void removeLastRegionFromSelection() {
+		if (allRegionsHaveSameText()) {
+			selectRegions(removeLastRegionButOne(getSelectedRegions()));
+		}
+	}
+}
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/RemoveLastMatchFromMultiSelectionHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/RemoveLastMatchFromMultiSelectionHandler.java
deleted file mode 100644
index 35be36d..0000000
--- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/RemoveLastMatchFromMultiSelectionHandler.java
+++ /dev/null
@@ -1,29 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2022 Dirk Steinkamp
- *
- * This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License 2.0
- * which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-2.0/
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Contributors:
- *     Dirk Steinkamp <dirk.steinkamp@gmx.de> - initial API and implementation
- *******************************************************************************/
-package org.eclipse.ui.internal.texteditor.multiselection;
-
-import org.eclipse.core.commands.ExecutionException;
-
-/**
- * Removes last selection region from a multi-selection.
- */
-public class RemoveLastMatchFromMultiSelectionHandler extends AbstractMultiSelectionHandler {
-
-	@Override
-	public void execute() throws ExecutionException {
-		if (allRegionsHaveSameText()) {
-			selectRegions(removeLastRegionButOne(getSelectedRegions()));
-		}
-	}
-}
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/StopMultiSelectionHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/StopMultiSelectionHandler.java
index fc28e7c..3672da1 100644
--- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/StopMultiSelectionHandler.java
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/StopMultiSelectionHandler.java
@@ -33,5 +33,6 @@
 		int caretOffset = getCaretOffset();
 		selectRegion(offsetAsCaretRegion(caretOffset));
 		setCaretOffset(caretOffset);
+		setAnchorRegion(null);
 	}
 }