diff options
author | Dirk Steinkamp | 2022-03-24 00:18:25 +0000 |
---|---|---|
committer | Mickael Istria | 2022-03-25 12:44:29 +0000 |
commit | 150da4b2675d18f53a2087b74f6e6bed65ce0472 (patch) | |
tree | 4fad9af8105ae50ee6b1b458eb786f52af58d7af | |
parent | 2f55fffaedd81f5cdd632687fb0f9a6c2eeea0b7 (diff) | |
download | eclipse.platform.text-master.tar.gz eclipse.platform.text-master.tar.xz eclipse.platform.text-master.zip |
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>
8 files changed, 482 insertions, 36 deletions
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 1f700b1672c..85b43634ae8 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 @@ public class TextMultiCaretSelectionCommandsTest { 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 @@ public class TextMultiCaretSelectionCommandsTest { 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 @@ public class TextMultiCaretSelectionCommandsTest { 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 @@ public class TextMultiCaretSelectionCommandsTest { // 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 @@ public class TextMultiCaretSelectionCommandsTest { 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 @@ public class TextMultiCaretSelectionCommandsTest { 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 @@ public class TextMultiCaretSelectionCommandsTest { 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 @@ public class TextMultiCaretSelectionCommandsTest { } @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 @@ public class TextMultiCaretSelectionCommandsTest { @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 @@ public class TextMultiCaretSelectionCommandsTest { } @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 @@ public class TextMultiCaretSelectionCommandsTest { } @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 @@ public class TextMultiCaretSelectionCommandsTest { 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 2e204f51a14..f647d810d8d 100644 --- a/org.eclipse.ui.workbench.texteditor/plugin.properties +++ b/org.eclipse.ui.workbench.texteditor/plugin.properties @@ -167,6 +167,10 @@ command.selectMultiSelectionUp.description = Search next matching region above a 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 9dea32dcd87..9c861c23c22 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 e8f7a40eab1..986afd6c063 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.Arrays; 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.BadLocationException; 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { } } - 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { 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; + } + + 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); + } - return newLineOffset + delta; + private int endOfLineOffset(int lineNo) throws BadLocationException { + return document.getLineOffset(lineNo) + document.getLineInformation(lineNo).getLength(); } private boolean initFrom(ExecutionEvent event) { @@ -319,7 +346,8 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { initTextEditor(); if (textEditor == null) return false; - document = getDocument(); + initDocument(); + initSourceViewer(); initAnchorRegion(); return true; } @@ -329,8 +357,13 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { 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 00000000000..6d445421550 --- /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 00000000000..1f12b4cd361 --- /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 678077e20a9..20ed24c66f2 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 @@ 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> + * 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 8205b3a296c..6a31f556dce 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 @@ 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> + * 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. */ |