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

Added two new commands that add a caret/selection in the line
above/below and the reverse operation. This operation is similar to
what is possible with block selection, but currently treats positions
after end of line differently (currently no automatic padding to fill up
shorter lines is performed).

moveOffsetByLines now respects the visual positions instead of purely
working with document offsets, which improves usability in case of
a different number of leading tabs in adjacent lines.

Change-Id: I01563e6ab148655d0dd6c34e2a89776a1ea0f9a9
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.text/+/192202
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 1f700b1..85b4363 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
@@ -56,10 +56,12 @@
 	private static final String ADD_ALL_MATCHES_TO_MULTI_SELECTION = "org.eclipse.ui.edit.text.select.addAllMatchesToMultiSelection";
 	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 MULTI_CARET_DOWN = "org.eclipse.ui.edit.text.select.multiCaretDown";
+	private static final String MULTI_CARET_UP = "org.eclipse.ui.edit.text.select.multiCaretUp";
 
 	private static final String LINE_1 = "private static String a;\n";
-	private static final String LINE_2 = "private static String b;\n";
-	private static final String LINE_3 = "private static String c;\n";
+	private static final String LINE_2 = "private static String b; // this is a little longer\n";
+	private static final String LINE_3 = "\t\tprivate static String c;\n";
 	private static final String LINE_4 = "private static String d";
 
 	private static final int L1_LEN = LINE_1.length();
@@ -101,7 +103,7 @@
 		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) },
+		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN + 2, 7) },
 				getSelection());
 
 		executeCommand(STOP_MULTI_SELECTION);
@@ -130,7 +132,7 @@
 		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) },
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 10, 6) },
 				getSelection());
 	}
 
@@ -153,12 +155,12 @@
 		// 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) },
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 10, 6) },
 				getSelection());
 
 		executeCommand(MULTI_SELECTION_DOWN);
 
-		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 8, 6),
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 10, 6),
 				new Region(L1_LEN + L2_LEN + L3_LEN + 8, 6) }, getSelection());
 	}
 
@@ -241,7 +243,7 @@
 		executeCommand(ADD_ALL_MATCHES_TO_MULTI_SELECTION);
 
 		assertEquals(7, widget.getCaretOffset());
-		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN, 7),
+		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN + 2, 7),
 				new Region(L1_LEN + L2_LEN + L3_LEN, 7) }, getSelection());
 	}
 
@@ -253,7 +255,7 @@
 		executeCommand(ADD_ALL_MATCHES_TO_MULTI_SELECTION);
 
 		assertEquals(7, widget.getCaretOffset());
-		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN, 7),
+		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN + 2, 7),
 				new Region(L1_LEN + L2_LEN + L3_LEN, 7) }, getSelection());
 	}
 
@@ -276,7 +278,7 @@
 		executeCommand(ADD_ALL_MATCHES_TO_MULTI_SELECTION);
 
 		assertEquals(7, widget.getCaretOffset());
-		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN, 7),
+		assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7), new Region(L1_LEN + L2_LEN + 2, 7),
 				new Region(L1_LEN + L2_LEN + L3_LEN, 7) }, getSelection());
 	}
 
@@ -292,8 +294,7 @@
 	}
 
 	@Test
-	public void testMultiSelectionUp_withSingleSelectionAndNoPreviousMatch_doesNothing()
-			throws Exception {
+	public void testMultiSelectionUp_withSingleSelectionAndNoPreviousMatch_doesNothing() throws Exception {
 		setSelection(new IRegion[] { new Region(8, 6) });
 		assertEquals(14, widget.getCaretOffset());
 
@@ -316,7 +317,8 @@
 
 	@Test
 	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) });
+		setSelection(
+				new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 10, 6) });
 		assertEquals(14, widget.getCaretOffset());
 
 		executeCommand(MULTI_SELECTION_UP);
@@ -326,8 +328,7 @@
 	}
 
 	@Test
-	public void testMultiSelectionUp_withTwoSelectionsAndAnchorAbove_reducesSelection()
-			throws Exception {
+	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
@@ -340,19 +341,18 @@
 	}
 
 	@Test
-	public void testMultiSelectionUp_withTwoSelectionsAndAnchorBelow_extendsSelection()
-			throws Exception {
-		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN + 8, 6) });
+	public void testMultiSelectionUp_withTwoSelectionsAndAnchorBelow_extendsSelection() throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN + 10, 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) },
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 10, 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) },
+				new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 10, 6) },
 				getSelection());
 	}
 
@@ -407,6 +407,265 @@
 		assertArrayEquals(new IRegion[] { new Region(7, 0) }, getSelection());
 	}
 
+	@Test
+	public void testMultiCaretDown_withCaret_addsCaretsInNextLines() throws Exception {
+		setSelection(new IRegion[] { new Region(0, 0) });
+		assertEquals(0, widget.getCaretOffset());
+
+		executeCommand(MULTI_CARET_DOWN);
+
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0), new Region(L1_LEN, 0) }, getSelection());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_DOWN);
+
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0), new Region(L1_LEN, 0), new Region(L1_LEN + L2_LEN, 0) },
+				getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretDown_withTwoCaretsAndAnchorRegionBelow_removesFirstCaret() throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN, 0) });
+		assertEquals(L1_LEN, widget.getCaretOffset());
+
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0), new Region(L1_LEN, 0) }, getSelection());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_DOWN);
+
+		assertEquals(L1_LEN, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN, 0) }, getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(L1_LEN, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretDown_withSingleSelection_addsSelectionInNextLine() throws Exception {
+		setSelection(new IRegion[] { new Region(0, 3) });
+		assertEquals(3, widget.getCaretOffset());
+
+		executeCommand(MULTI_CARET_DOWN);
+
+		assertEquals(3, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 3), new Region(L1_LEN, 3) }, getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(3, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(3, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretDown_withSingleCaretAtEndOfLongerLine_addsCaretAtEndOfNextLine() throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN, 0) });
+		assertEquals(L1_LEN + L2_LEN, widget.getCaretOffset());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_DOWN);
+
+		assertEquals(L1_LEN + L2_LEN, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + L2_LEN, 0), new Region(L1_LEN + L2_LEN + L3_LEN, 0) },
+				getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(L1_LEN + L2_LEN, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + L2_LEN, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretDown_withSingleCaretInLineAboveLineWithTabs_addsCaretInNextLineRespectingTabs()
+			throws Exception {
+		widget.setTabs(4); // Make default explicit
+		setSelection(new IRegion[] { new Region(L1_LEN + 8, 0) });
+		assertEquals(L1_LEN + 8, widget.getCaretOffset());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_DOWN);
+
+		assertEquals(L1_LEN + 8, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 0), new Region(L1_LEN + L2_LEN + 2, 0) },
+				getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(L1_LEN + 8, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretDown_withSingleCaretInLineWithTabs_addsCaretInNextLineRespectingTabs() throws Exception {
+		widget.setTabs(4); // Make default explicit
+		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN + 2, 0) });
+		assertEquals(L1_LEN + L2_LEN + 2, widget.getCaretOffset());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_DOWN);
+
+		assertEquals(L1_LEN + L2_LEN + 2, widget.getCaretOffset());
+		assertArrayEquals(
+				new IRegion[] { new Region(L1_LEN + L2_LEN + 2, 0), new Region(L1_LEN + L2_LEN + L3_LEN + 8, 0) },
+				getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(L1_LEN + L2_LEN + 2, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + L2_LEN + 2, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretDown_withSingleCaretAtEndOfText_doesNotChangeCaret() 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());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_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, 0) }, getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(L1_LEN + L2_LEN + L3_LEN + L4_LEN, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + L2_LEN + L3_LEN + L4_LEN, 0) }, getSelection());
+	}
+
+	/////////////////////////////////////////////////////
+	@Test
+	public void testMultiCaretUp_withCaret_addsCaretsInPreviousLines() throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN, 0) });
+		assertEquals(L1_LEN + L2_LEN, widget.getCaretOffset());
+
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(L1_LEN, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN, 0), new Region(L1_LEN + L2_LEN, 0) }, getSelection());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0), new Region(L1_LEN, 0), new Region(L1_LEN + L2_LEN, 0) },
+				getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretUp_withTwoCaretsAndAnchorRegionAbove_removesLastCaret() throws Exception {
+		setSelection(new IRegion[] { new Region(0, 0) });
+		assertEquals(0, widget.getCaretOffset());
+
+		executeCommand(MULTI_CARET_DOWN);
+
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0), new Region(L1_LEN, 0) }, getSelection());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0) }, getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretUp_withSingleSelection_addsSelectionInPreviousLine() throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN, 3) });
+		assertEquals(L1_LEN + 3, widget.getCaretOffset());
+
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(3, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 3), new Region(L1_LEN, 3) }, getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(3, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(3, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretUp_withSingleCaretAtEndOfLongerLine_addsCaretAtEndOfPreviousLine() throws Exception {
+		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN, 0) });
+		assertEquals(L1_LEN + L2_LEN, widget.getCaretOffset());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(L1_LEN, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN, 0), new Region(L1_LEN + L2_LEN, 0) }, getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(L1_LEN, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretUp_withSingleCaretInLineBelowLineWithTabs_addsCaretInPreviousLineRespectingTabs()
+			throws Exception {
+		widget.setTabs(4); // Make default explicit
+		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN + L3_LEN + 8, 0) });
+		assertEquals(L1_LEN + L2_LEN + L3_LEN + 8, widget.getCaretOffset());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(L1_LEN + L2_LEN + 2, widget.getCaretOffset());
+		assertArrayEquals(
+				new IRegion[] { new Region(L1_LEN + L2_LEN + 2, 0), new Region(L1_LEN + L2_LEN + L3_LEN + 8, 0) },
+				getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(L1_LEN + L2_LEN + 2, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + L2_LEN + 2, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretUp_withSingleCaretInLineWithTabs_addsCaretInPreviousLineRespectingTabs()
+			throws Exception {
+		widget.setTabs(4); // Make default explicit
+		setSelection(new IRegion[] { new Region(L1_LEN + L2_LEN + 2, 0) });
+		assertEquals(L1_LEN + L2_LEN + 2, widget.getCaretOffset());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(L1_LEN + 8, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 0), new Region(L1_LEN + L2_LEN + 2, 0) },
+				getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(L1_LEN + 8, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 0) }, getSelection());
+	}
+
+	@Test
+	public void testMultiCaretUp_withSingleCaretAtBeginningOfText_doesNotChangeCaret() throws Exception {
+		setSelection(new IRegion[] { new Region(0, 0) });
+		assertEquals(0, widget.getCaretOffset());
+
+		widget.setSize(800, 500); // make sure the widget is not size (0,0)
+		executeCommand(MULTI_CARET_UP);
+
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0) }, getSelection());
+
+		executeCommand(STOP_MULTI_SELECTION);
+		assertEquals(0, widget.getCaretOffset());
+		assertArrayEquals(new IRegion[] { new Region(0, 0) }, getSelection());
+	}
+
 	// Helper methods
 
 	private void executeCommand(String commandId) throws Exception {
diff --git a/org.eclipse.ui.workbench.texteditor/plugin.properties b/org.eclipse.ui.workbench.texteditor/plugin.properties
index 2e204f5..f647d81 100644
--- a/org.eclipse.ui.workbench.texteditor/plugin.properties
+++ b/org.eclipse.ui.workbench.texteditor/plugin.properties
@@ -167,6 +167,10 @@
 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.multiCaretUp.description=Add a new caret/multi selection above the current line, or remove the last caret/multi selection 
+command.multiCaretUp.name=Multi caret up
+command.multiCaretDown.description=Add a new caret/multi selection below the current line, or remove the first caret/multi selection 
+command.multiCaretDown.name=Multi caret down
 command.selectWordNext.description = Select the next word
 command.selectWordNext.name = Select Next Word
 command.selectWordPrevious.description = Select the previous word
diff --git a/org.eclipse.ui.workbench.texteditor/plugin.xml b/org.eclipse.ui.workbench.texteditor/plugin.xml
index 9dea32d..9c861c2 100644
--- a/org.eclipse.ui.workbench.texteditor/plugin.xml
+++ b/org.eclipse.ui.workbench.texteditor/plugin.xml
@@ -329,6 +329,18 @@
 	        id="org.eclipse.ui.edit.text.select.stopMultiSelection">
 	  </command>
 	  <command
+	        name="%command.multiCaretUp.name"
+	        description="%command.multiCaretUp.description"
+	        categoryId="org.eclipse.ui.category.textEditor"
+	        id="org.eclipse.ui.edit.text.select.multiCaretUp">
+	  </command>	  
+	  <command
+	        name="%command.multiCaretDown.name"
+	        description="%command.multiCaretDown.description"
+	        categoryId="org.eclipse.ui.category.textEditor"
+	        id="org.eclipse.ui.edit.text.select.multiCaretDown">
+	  </command>	  
+	  <command
 	        name="%command.deletePrevious.name"
 	        description="%command.deletePrevious.description"
 	        categoryId="org.eclipse.ui.category.textEditor"
@@ -1422,6 +1434,24 @@
             </with>
          </enabledWhen>
       </handler>
+      <handler
+            class="org.eclipse.ui.internal.texteditor.multiselection.MultiCaretUpHandler"
+            commandId="org.eclipse.ui.edit.text.select.multiCaretUp">
+         <enabledWhen>
+            <with variable="activeEditor">
+              <adapt type="org.eclipse.ui.texteditor.ITextEditor"/>
+            </with>
+         </enabledWhen>
+      </handler>
+      <handler
+            class="org.eclipse.ui.internal.texteditor.multiselection.MultiCaretDownHandler"
+            commandId="org.eclipse.ui.edit.text.select.multiCaretDown">
+         <enabledWhen>
+            <with variable="activeEditor">
+              <adapt type="org.eclipse.ui.texteditor.ITextEditor"/>
+            </with>
+         </enabledWhen>
+      </handler>
    </extension>
    <extension
          point="org.eclipse.ui.menus">
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 e8f7a40..986afd6 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
@@ -18,6 +18,7 @@
 import java.util.List;
 
 import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.graphics.Point;
 import org.eclipse.swt.widgets.Control;
 
 import org.eclipse.core.commands.AbstractHandler;
@@ -32,6 +33,8 @@
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IMultiTextSelection;
 import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.ITextViewerExtension5;
 import org.eclipse.jface.text.MultiTextSelection;
 import org.eclipse.jface.text.Region;
 
@@ -61,6 +64,11 @@
 	private ExecutionEvent event;
 	private ITextEditor textEditor;
 	private IDocument document;
+	/**
+	 * SourceViewer Might be <code>null</code>, if {@link #textEditor} doesn't
+	 * implement this interface.
+	 */
+	private ITextViewerExtension5 sourceViewer;
 
 	/**
 	 * This method needs to be overwritten from subclasses to handle the event.
@@ -264,7 +272,7 @@
 		}
 	}
 
-	private IRegion createRegionIfValid(int offset, int length) {
+	protected IRegion createRegionIfValid(int offset, int length) {
 		if ((offset < 0) || (offset > document.getLength()))
 			return null;
 
@@ -294,24 +302,43 @@
 		return regions;
 	}
 
-	private int offsetInNextLine(int offset) throws BadLocationException {
+	protected int offsetInNextLine(int offset) throws BadLocationException {
 		return moveOffsetByLines(offset, 1);
 	}
 
-	private int offsetInPreviousLine(int offset) throws BadLocationException {
+	protected 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;
+		int newLineNo = document.getLineOfOffset(offset) + lineDelta;
 		if ((newLineNo < 0) || (newLineNo >= document.getNumberOfLines()))
 			return -1;
 
-		int newLineOffset = document.getLineOffset(newLineNo);
-		int delta = offset - document.getLineOffset(lineNo);
+		int newOffset;
+		if (sourceViewer == null) {
+			// we don't have a sourceViewer and thus as a fallback
+			// assume the widget offsets are identical to the document offsets
+			newOffset = moveWidgetOffsetByLines(offset, lineDelta);
+		} else {
+			int widgetOffset = sourceViewer.modelOffset2WidgetOffset(offset);
+			int newWidgetOffset = moveWidgetOffsetByLines(widgetOffset, lineDelta);
+			newOffset = sourceViewer.widgetOffset2ModelOffset(newWidgetOffset);
+		}
+		if (newOffset == -1) {
+			return endOfLineOffset(newLineNo);
+		}
+		return newOffset;
+	}
 
-		return newLineOffset + delta;
+	private int moveWidgetOffsetByLines(int widgetOffset, int lineDelta) throws BadLocationException {
+		Point location = getWidget().getLocationAtOffset(widgetOffset);
+		Point newLocation = new Point(location.x, location.y + lineDelta * getWidget().getLineHeight(widgetOffset));
+		return getWidget().getOffsetAtPoint(newLocation);
+	}
+
+	private int endOfLineOffset(int lineNo) throws BadLocationException {
+		return document.getLineOffset(lineNo) + document.getLineInformation(lineNo).getLength();
 	}
 
 	private boolean initFrom(ExecutionEvent event) {
@@ -319,7 +346,8 @@
 		initTextEditor();
 		if (textEditor == null)
 			return false;
-		document = getDocument();
+		initDocument();
+		initSourceViewer();
 		initAnchorRegion();
 		return true;
 	}
@@ -329,8 +357,13 @@
 		textEditor = Adapters.adapt(editor, ITextEditor.class);
 	}
 
-	private IDocument getDocument() {
-		return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
+	private void initDocument() {
+		document = textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput());
+	}
+
+	private void initSourceViewer() {
+		ITextViewer textViewer = textEditor.getAdapter(ITextViewer.class);
+		sourceViewer = Adapters.adapt(textViewer, ITextViewerExtension5.class);
 	}
 
 	private IRegion[] toArray(List<IRegion> regions) {
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiCaretDownHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiCaretDownHandler.java
new file mode 100644
index 0000000..6d44542
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiCaretDownHandler.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * 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.BadLocationException;
+import org.eclipse.jface.text.IRegion;
+
+/**
+ * Handler to change the current set of multi carets/selections downwards. This
+ * might either mean to add a caret/selection below by adding a new
+ * caret/selection at the same line offset in the next line, or reduce the
+ * number of carets/selections by removing the first caret/selection range. This
+ * depends on the selection a multi caret/selection command was invoked with the
+ * first time -- that selection is remembered as an "anchor" to which successive
+ * calls are related as reference selection.<br>
+ */
+public class MultiCaretDownHandler extends AbstractMultiSelectionHandler {
+
+	@Override
+	public void execute() throws ExecutionException {
+		if (selectionIsAboveAnchorRegion()) {
+			removeFirstRegionFromSelection();
+		} else {
+			extendSelectionWithSamePositionInNextLine();
+		}
+	}
+
+	private void extendSelectionWithSamePositionInNextLine() throws ExecutionException {
+		IRegion[] regions = getSelectedRegions();
+		if (regions == null || regions.length == 0) {
+			return;
+		}
+		try {
+			IRegion lastRegion = regions[regions.length - 1];
+			int newOffset = offsetInNextLine(lastRegion.getOffset());
+			IRegion nextLineRegion = createRegionIfValid(newOffset, lastRegion.getLength());
+			selectRegions(addRegion(regions, nextLineRegion));
+		} catch (BadLocationException e) {
+			throw new ExecutionException("Internal error in extendSelectionWithSamePositionInNextLine", e);
+		}
+	}
+
+	private void removeFirstRegionFromSelection() {
+		selectRegions(removeFirstRegionButOne(getSelectedRegions()));
+	}
+}
diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiCaretUpHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiCaretUpHandler.java
new file mode 100644
index 0000000..1f12b4c
--- /dev/null
+++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/MultiCaretUpHandler.java
@@ -0,0 +1,59 @@
+/*******************************************************************************
+ * 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.BadLocationException;
+import org.eclipse.jface.text.IRegion;
+
+/**
+ * Handler to change the current set of multi carets/selections upwards. This
+ * might either mean to add a caret/selection above by adding a new
+ * caret/selection at the same line offset in the previous line, or reduce the
+ * number of carets/selections by removing the last caret/selection range. This
+ * depends on the selection a multi caret/selection command was invoked with the
+ * first time -- that selection is remembered as an "anchor" to which successive
+ * calls are related as reference selection.<br>
+ */
+public class MultiCaretUpHandler extends AbstractMultiSelectionHandler {
+
+	@Override
+	public void execute() throws ExecutionException {
+		if (selectionIsBelowAnchorRegion()) {
+			removeLastRegionFromSelection();
+		} else {
+			extendSelectionWithSamePositionInPreviousLine();
+		}
+	}
+
+	private void extendSelectionWithSamePositionInPreviousLine() throws ExecutionException {
+		IRegion[] regions = getSelectedRegions();
+		if (regions == null || regions.length == 0) {
+			return;
+		}
+		try {
+			IRegion firstRegion = regions[0];
+			int newOffset = offsetInPreviousLine(firstRegion.getOffset());
+			IRegion previousLineRegion = createRegionIfValid(newOffset, firstRegion.getLength());
+			selectRegions(addRegion(regions, previousLineRegion));
+		} catch (BadLocationException e) {
+			throw new ExecutionException("Internal error in extendSelectionWithSamePositionInPreviousLine", e);
+		}
+	}
+
+	private void removeLastRegionFromSelection() {
+			selectRegions(removeLastRegionButOne(getSelectedRegions()));
+	}
+}
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
index 678077e..20ed24c 100644
--- 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
@@ -20,9 +20,10 @@
 /**
  * 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>
+ * by removing the first selection range. This depends on the selection a multi
+ * caret/selection 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.
  */
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
index 8205b3a..6a31f55 100644
--- 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
@@ -20,9 +20,10 @@
 /**
  * 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>
+ * removing the last selection range. This depends on the selection a multi
+ * caret/selection 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.
  */