diff options
author | Dirk Steinkamp | 2022-03-13 18:03:44 +0000 |
---|---|---|
committer | Mickael Istria | 2022-03-15 09:56:00 +0000 |
commit | 13e3e7a5866fb13f479253382030907238a54ee2 (patch) | |
tree | b2215f6e60d4e9b53550c1cb6e7af8bbf307a0cb | |
parent | 579ca5b681fe05936c8272af9109c79c4e4fb1a0 (diff) | |
download | eclipse.platform.text-13e3e7a5866fb13f479253382030907238a54ee2.tar.gz eclipse.platform.text-13e3e7a5866fb13f479253382030907238a54ee2.tar.xz eclipse.platform.text-13e3e7a5866fb13f479253382030907238a54ee2.zip |
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>
9 files changed, 340 insertions, 108 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 4ddeb9f2db5..3165209eb5d 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 @@ import org.eclipse.ui.texteditor.AbstractTextEditor; * 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 @@ public class TextMultiCaretSelectionCommandsTest { } @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 @@ public class TextMultiCaretSelectionCommandsTest { } @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 @@ public class TextMultiCaretSelectionCommandsTest { } @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 @@ public class TextMultiCaretSelectionCommandsTest { } @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 @@ public class TextMultiCaretSelectionCommandsTest { 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 f7bf56102e9..2e204f51a14 100644 --- a/org.eclipse.ui.workbench.texteditor/plugin.properties +++ b/org.eclipse.ui.workbench.texteditor/plugin.properties @@ -161,10 +161,10 @@ command.selectWindowStart.description = Select to the start of the window 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 f8d54ff2e24..70d47cf569d 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 f54d153cf7b..ba42de96dc7 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 @@ import org.eclipse.ui.texteditor.ITextEditorExtension5; * 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { /** * 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { } 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { } protected IRegion findNextMatch(IRegion region) throws ExecutionException { - String fullText = getFullText(); try { - String searchString = getTextOfRegion(region); - - int matchPos = fullText.indexOf(searchString, offsetAfter(region)); - if (matchPos < 0) - return null; - - return new Region(matchPos, region.getLength()); + if (region.getLength() == 0) { + return offsetAsCaretRegion(offsetInNextLine(region.getOffset())); + } else { + String searchString = getTextOfRegion(region); + + 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { 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 @@ abstract class AbstractMultiSelectionHandler extends AbstractHandler { 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 b9d09fc7cf1..189ea105aa6 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 @@ public class AddAllMatchesToMultiSelectionHandler extends AbstractMultiSelection 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/MultiSelectionDownHandler.java index 16f9d479d40..678077e20a9 100644 --- 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/MultiSelectionDownHandler.java @@ -18,16 +18,22 @@ 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. + * 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 AddNextMatchToMultiSelectionHandler extends AbstractMultiSelectionHandler { +public class MultiSelectionDownHandler extends AbstractMultiSelectionHandler { @Override public void execute() throws ExecutionException { if (nothingSelected()) { selectIdentifierUnderCaret(); + } else if (selectionIsAboveAnchorRegion()) { + removeFirstRegionFromSelection(); } else { extendSelectionToNextMatch(); } @@ -40,4 +46,10 @@ public class AddNextMatchToMultiSelectionHandler extends AbstractMultiSelectionH 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 00000000000..8205b3a296c --- /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 35be36dd579..00000000000 --- 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 fc28e7c922e..3672da17c69 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 @@ public class StopMultiSelectionHandler extends AbstractMultiSelectionHandler { int caretOffset = getCaretOffset(); selectRegion(offsetAsCaretRegion(caretOffset)); setCaretOffset(caretOffset); + setAnchorRegion(null); } } |