diff options
author | Dirk Steinkamp | 2022-03-04 20:14:28 +0000 |
---|---|---|
committer | Mickael Istria | 2022-03-11 12:32:34 +0000 |
commit | 1a27e19c6ff1267be05cb2b833b2f977d7da2dfc (patch) | |
tree | cc5ea01ddd300274783fbee6bf5da00ab13cfca7 | |
parent | 38f7a5e579ba49611f47d47b1749b0fe148d34f6 (diff) | |
download | eclipse.platform.text-1a27e19c6ff1267be05cb2b833b2f977d7da2dfc.tar.gz eclipse.platform.text-1a27e19c6ff1267be05cb2b833b2f977d7da2dfc.tar.xz eclipse.platform.text-1a27e19c6ff1267be05cb2b833b2f977d7da2dfc.zip |
Bug 576377 - Provide shortcuts/commands for incrementalI20220314-1800I20220313-1800I20220312-1800I20220312-0100I20220311-1800
multiselection/multiple carets in text editors
Add various commands for multi-selection (intended for keyboard usage,
keyboard shortcuts are just suggestions):
- AddAllMatchesToMultiSelection (e.g. CTRL-ALT-SHIFT-J)
- AddNextMatchToMultiSelection (e.g. ALT-J)
- RemoveLastMatchFromMultiSelectionHandler (e.g. ALT-SHIFT-J)
- StopMultiSelectionHandler (e.g. ESC)
Change-Id: Id9add4daad15495ee00c76d8a5a7c5dc5608f506
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.text/+/191500
Tested-by: Platform Bot <platform-bot@eclipse.org>
Reviewed-by: Mickael Istria <mistria@redhat.com>
12 files changed, 897 insertions, 9 deletions
diff --git a/org.eclipse.ui.editors.tests/META-INF/MANIFEST.MF b/org.eclipse.ui.editors.tests/META-INF/MANIFEST.MF index 47dd5a5b647..5ece30fa9b8 100644 --- a/org.eclipse.ui.editors.tests/META-INF/MANIFEST.MF +++ b/org.eclipse.ui.editors.tests/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %Plugin.name Bundle-SymbolicName: org.eclipse.ui.editors.tests;singleton:=true -Bundle-Version: 3.12.400.qualifier +Bundle-Version: 3.12.500.qualifier Bundle-Vendor: %Plugin.providerName Bundle-Localization: plugin Export-Package: org.eclipse.ui.editors.tests @@ -11,7 +11,7 @@ Require-Bundle: org.junit;bundle-version="4.12.0", org.eclipse.jface;bundle-version="[3.5.0,4.0.0)", org.eclipse.text;bundle-version="[3.5.0,4.0.0)", - org.eclipse.ui.workbench.texteditor;bundle-version="[3.5.0,4.0.0)", + org.eclipse.ui.workbench.texteditor;bundle-version="[3.16.500,4.0.0)", org.eclipse.ui.editors;bundle-version="[3.5.0,4.0.0)", org.eclipse.ui.workbench;bundle-version="[3.5.0,4.0.0)", org.eclipse.core.resources;bundle-version="[3.14.0,4.0.0)", diff --git a/org.eclipse.ui.editors.tests/pom.xml b/org.eclipse.ui.editors.tests/pom.xml index 81baf23f58e..26ee84a16a6 100644 --- a/org.eclipse.ui.editors.tests/pom.xml +++ b/org.eclipse.ui.editors.tests/pom.xml @@ -18,7 +18,7 @@ <relativePath>../tests-pom/</relativePath> </parent> <artifactId>org.eclipse.ui.editors.tests</artifactId> - <version>3.12.400-SNAPSHOT</version> + <version>3.12.500-SNAPSHOT</version> <packaging>eclipse-test-plugin</packaging> <properties> <testSuite>${project.artifactId}</testSuite> diff --git a/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java b/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java index 74c52d215c7..4dea7310cc7 100644 --- a/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java +++ b/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java @@ -1,5 +1,5 @@ -/******************************************************************************* - * Copyright (c) 2000, 2019 IBM Corporation and others. +/************************************************************************************************ + * Copyright (c) 2000, 2022 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -11,7 +11,8 @@ * Contributors: * IBM Corporation - initial API and implementation * Mickael Istria (Red Hat Inc.) - [484157] Add zoom test - *******************************************************************************/ + * Dirk Steinkamp <dirk.steinkamp@gmx.de> - [576377] Add multi caret selection commands test + ************************************************************************************************/ package org.eclipse.ui.editors.tests; import org.junit.runner.RunWith; @@ -36,7 +37,9 @@ import org.junit.runners.Suite.SuiteClasses; TextFileDocumentProviderTest.class, StatusEditorTest.class, TextNavigationTest.class, - LargeFileTest.class, CaseActionTest.class + LargeFileTest.class, CaseActionTest.class, + TextMultiCaretNavigationTest.class, + TextMultiCaretSelectionCommandsTest.class, }) public class EditorsTestSuite { // see @SuiteClasses 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 new file mode 100644 index 00000000000..4ddeb9f2db5 --- /dev/null +++ b/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextMultiCaretSelectionCommandsTest.java @@ -0,0 +1,360 @@ +/******************************************************************************* + * 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.editors.tests; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.widgets.Control; + +import org.eclipse.core.commands.Command; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.filesystem.EFS; + +import org.eclipse.core.runtime.CoreException; + +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IMultiTextSelection; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.MultiTextSelection; +import org.eclipse.jface.text.Region; + +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.commands.ICommandService; +import org.eclipse.ui.ide.IDE; + +import org.eclipse.ui.texteditor.AbstractTextEditor; + +/* + * Note: this test would better fit in the org.eclipse.ui.workbench.texteditor bundle, but initializing + * 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 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 STOP_MULTI_SELECTION = "org.eclipse.ui.edit.text.select.stopMultiSelection"; + + 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_4 = "private static String d"; + + private static final int L1_LEN = LINE_1.length(); + private static final int L2_LEN = LINE_2.length(); + private static final int L3_LEN = LINE_3.length(); + private static final int L4_LEN = LINE_4.length(); + + private static File file; + private static AbstractTextEditor editor; + private static StyledText widget; + + @Before + public void setUpBeforeClass() throws IOException, PartInitException, CoreException { + file = File.createTempFile(TextMultiCaretSelectionCommandsTest.class.getName(), ".txt"); + Files.write(file.toPath(), (LINE_1 + LINE_2 + LINE_3 + LINE_4) // + .getBytes()); + editor = (AbstractTextEditor) IDE.openEditorOnFileStore( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), EFS.getStore(file.toURI())); + widget = (StyledText) editor.getAdapter(Control.class); + } + + @After + public void tearDown() { + editor.dispose(); + file.delete(); + } + + @Test + public void testAddNextMatch_withFirstIdentifierSelected_addsIdenticalIdentifiersToSelection() throws Exception { + setSelection(new IRegion[] { new Region(0, 7) }); + assertEquals(7, widget.getCaretOffset()); + + executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION); + + assertEquals(7, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7) }, getSelection()); + + executeCommand(ADD_NEXT_MATCH_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) }, + getSelection()); + + executeCommand(STOP_MULTI_SELECTION); + assertEquals(7, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(7, 0) }, getSelection()); + } + + @Test + public void testAddNextMatch_withSecondIdentifierSelectedIdentifier_addsNextOccurenceToSelection() + throws Exception { + setSelection(new IRegion[] { new Region(8, 6) }); + assertEquals(14, widget.getCaretOffset()); + + executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION); + + assertEquals(14, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6) }, getSelection()); + } + + @Test + public void testAddNextMatch_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); + + assertEquals(L1_LEN + 14, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6), new Region(L1_LEN + L2_LEN + 8, 6) }, + getSelection()); + } + + @Test + public void testAddNextMatch_withCaretInFirstIdentifier_selectsFullIdentifier() throws Exception { + setSelection(new IRegion[] { new Region(1, 0) }); + assertEquals(1, widget.getCaretOffset()); + + executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION); + + assertEquals(7, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(0, 7) }, getSelection()); + } + + @Test + public void testAddNextMatch_withCaretInSecondIdentifier_selectsFullIdentifier() throws Exception { + setSelection(new IRegion[] { new Region(11, 0) }); + assertEquals(11, widget.getCaretOffset()); + + executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION); + + assertEquals(14, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(8, 6) }, getSelection()); + } + + @Test + public void testAddNextMatch_withCaretBetweenIdentifierCharAndNonIdentifierChar_selectsFullIdentifier() + throws Exception { + setSelection(new IRegion[] { new Region(23, 0) }); + assertEquals(23, widget.getCaretOffset()); + + executeCommand(ADD_NEXT_MATCH_TO_MULTI_SELECTION); + + assertEquals(23, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(22, 1) }, getSelection()); + } + + @Test + public void testAddNextMatch_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); + + assertEquals(L1_LEN + 14, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(L1_LEN + 8, 6) }, getSelection()); + } + + @Test + public void testAddNextMatch_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); + + 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 { + 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); + + 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()); + } + + @Test + public void testAddAllMatches_withSingleSelection_selectsAllOccurences() throws Exception { + setSelection(new IRegion[] { new Region(0, 7) }); + assertEquals(7, widget.getCaretOffset()); + + 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), + new Region(L1_LEN + L2_LEN + L3_LEN, 7) }, getSelection()); + } + + @Test + public void testAddAllMatches_withDoubleSelectionOfSameText_selectsAllOccurences() throws Exception { + setSelection(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7) }); + assertEquals(7, widget.getCaretOffset()); + + 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), + new Region(L1_LEN + L2_LEN + L3_LEN, 7) }, getSelection()); + } + + @Test + public void testAddAllMatches_withDoubleSelectionOfDifferentTexts_doesNotChangeSelection() throws Exception { + setSelection(new IRegion[] { new Region(0, 7), new Region(8, 7) }); + assertEquals(7, widget.getCaretOffset()); + + executeCommand(ADD_ALL_MATCHES_TO_MULTI_SELECTION); + + assertEquals(7, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(0, 7), new Region(8, 7) }, getSelection()); + } + + @Test + public void testAddAllMatches_withCaretInIdentifier_selectsAllOccurencesOfIdentifier() throws Exception { + setSelection(new IRegion[] { new Region(2, 0) }); + assertEquals(2, widget.getCaretOffset()); + + 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), + new Region(L1_LEN + L2_LEN + L3_LEN, 7) }, getSelection()); + } + + @Test + public void testRemoveLastMatchFromMultiSelection_withCaretInIdentifier_doesNothing() throws Exception { + setSelection(new IRegion[] { new Region(L1_LEN + 11, 0) }); + assertEquals(L1_LEN + 11, widget.getCaretOffset()); + + executeCommand(REMOVE_LAST_MATCH_FROM_MULTI_SELECTION); + + assertEquals(L1_LEN + 11, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(L1_LEN + 11, 0) }, getSelection()); + } + + @Test + public void testRemoveLastMatchFromMultiSelection_withSingleSelection_doesNothing() throws Exception { + setSelection(new IRegion[] { new Region(8, 6) }); + assertEquals(14, widget.getCaretOffset()); + + executeCommand(REMOVE_LAST_MATCH_FROM_MULTI_SELECTION); + + assertEquals(14, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(8, 6) }, getSelection()); + } + + @Test + public void testRemoveLastMatchFromMultiSelection_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); + + assertEquals(14, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(8, 6) }, getSelection()); + } + + @Test + public void testRemoveLastMatchFromMultiSelection_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); + + assertEquals(14, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(8, 6), new Region(L1_LEN + 8, 6) }, getSelection()); + } + + @Test + public void testStopMultiSelection_withSingleSelection_doesNotChangeSelectionOrCaretOffset() throws Exception { + setSelection(new IRegion[] { new Region(0, 7) }); + + assertEquals(7, widget.getCaretOffset()); + + executeCommand(STOP_MULTI_SELECTION); + assertEquals(7, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(0, 7) }, getSelection()); + } + + @Test + public void testStopMultiSelection_withMultiSelection_revokesSelectionAndKeepsFirstCaretOffset() throws Exception { + setSelection(new IRegion[] { new Region(0, 7), new Region(L1_LEN, 7) }); + + assertEquals(7, widget.getCaretOffset()); + + executeCommand(STOP_MULTI_SELECTION); + assertEquals(7, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(7, 0) }, getSelection()); + } + + @Test + public void testStopMultiSelection_withMultiSelectionAndCaretAtBeginning_revokesSelectionAndKeepsFirstCaretOffset() + throws Exception { + setSelection(new IRegion[] { new Region(0, 0), new Region(0, 7), new Region(L1_LEN, 7) }); + assertEquals(0, widget.getCaretOffset()); + + executeCommand(STOP_MULTI_SELECTION); + assertEquals(0, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(0, 0) }, getSelection()); + } + + @Test + public void testStopMultiSelection_withMultiSelectionAndCaretAfterLastSelection_revokesSelectionAndKeepsCaretOffset() + throws Exception { + 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); + + // TODO How to place the caret at the end without dismissing the + // selection? Should rather be 57 + assertEquals(7, widget.getCaretOffset()); + + executeCommand(STOP_MULTI_SELECTION); + assertEquals(7, widget.getCaretOffset()); + assertArrayEquals(new IRegion[] { new Region(7, 0) }, getSelection()); + } + + // Helper methods + + private void executeCommand(String commandId) throws Exception { + Command command = PlatformUI.getWorkbench().getService(ICommandService.class).getCommand(commandId); + command.executeWithChecks(new ExecutionEvent(command, Collections.EMPTY_MAP, null, null)); + } + + private void setSelection(IRegion[] regions) { + IDocument document = editor.getDocumentProvider().getDocument(editor.getEditorInput()); + editor.getSelectionProvider().setSelection(new MultiTextSelection(document, regions)); + } + + private IRegion[] getSelection() { + return ((IMultiTextSelection) editor.getSelectionProvider().getSelection()).getRegions(); + } +} diff --git a/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF b/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF index 64ccad74f0f..de37f77a8d1 100644 --- a/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF +++ b/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.ui.workbench.texteditor; singleton:=true -Bundle-Version: 3.16.400.qualifier +Bundle-Version: 3.16.500.qualifier Bundle-Activator: org.eclipse.ui.internal.texteditor.TextEditorPlugin Bundle-ActivationPolicy: lazy Bundle-Vendor: %providerName diff --git a/org.eclipse.ui.workbench.texteditor/plugin.properties b/org.eclipse.ui.workbench.texteditor/plugin.properties index f2b4831e9a5..f7bf56102e9 100644 --- a/org.eclipse.ui.workbench.texteditor/plugin.properties +++ b/org.eclipse.ui.workbench.texteditor/plugin.properties @@ -1,5 +1,5 @@ ############################################################################### -# Copyright (c) 2000, 2015 IBM Corporation and others. +# Copyright (c) 2000, 2022 IBM Corporation and others. # # This program and the accompanying materials # are made available under the terms of the Eclipse Public License 2.0 @@ -14,6 +14,7 @@ # Daesung Ha <nberserk@gmail.com> - update recenter command description # Mickael Istria (Red Hat Inc.) - 469918 Zoom In/Out # Angelo Zerr <angelo.zerr@gmail.com> - [CodeMining] Provide extension point for CodeMining - Bug 528419 +# Dirk Steinkamp <dirk.steinkamp@gmx.de> - [576377] Add multi caret selection commands ############################################################################### pluginName= Text Editor Framework providerName= Eclipse.org @@ -158,6 +159,14 @@ command.selectWindowEnd.description = Select to the end of the window command.selectWindowEnd.name = Select Window End 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.stopMultiSelection.description = Unselects all multi-selections returning to a single cursor +command.stopMultiSelection.name = End multi-selection 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 18c9f928a8b..f8d54ff2e24 100644 --- a/org.eclipse.ui.workbench.texteditor/plugin.xml +++ b/org.eclipse.ui.workbench.texteditor/plugin.xml @@ -305,6 +305,30 @@ id="org.eclipse.ui.edit.text.select.windowEnd"> </command> <command + name="%command.selectAddAllMatchesToMultiSelection.name" + description="%command.selectAddAllMatchesToMultiSelection.description" + categoryId="org.eclipse.ui.category.textEditor" + id="org.eclipse.ui.edit.text.select.addAllMatchesToMultiSelection"> + </command> + <command + name="%command.selectAddNextMatchToMultiSelection.name" + description="%command.selectAddNextMatchToMultiSelection.description" + categoryId="org.eclipse.ui.category.textEditor" + id="org.eclipse.ui.edit.text.select.addNextMatchToMultiSelection"> + </command> + <command + name="%command.selectRemoveLastMatchFromMultiSelection.name" + description="%command.selectAddNextMatchToMultiSelection.description" + categoryId="org.eclipse.ui.category.textEditor" + id="org.eclipse.ui.edit.text.select.removeLastMatchFromMultiSelection"> + </command> + <command + name="%command.stopMultiSelection.name" + description="%command.stopMultiSelection.description" + categoryId="org.eclipse.ui.category.textEditor" + id="org.eclipse.ui.edit.text.select.stopMultiSelection"> + </command> + <command name="%command.deletePrevious.name" description="%command.deletePrevious.description" categoryId="org.eclipse.ui.category.textEditor" @@ -1365,6 +1389,54 @@ </with> </enabledWhen> </handler> + <handler + class="org.eclipse.ui.internal.texteditor.multiselection.AddAllMatchesToMultiSelectionHandler" + commandId="org.eclipse.ui.edit.text.select.addAllMatchesToMultiSelection"> + <enabledWhen> + <with + variable="activeEditor"> + <instanceof + value="org.eclipse.ui.texteditor.ITextEditor"> + </instanceof> + </with> + </enabledWhen> + </handler> + <handler + class="org.eclipse.ui.internal.texteditor.multiselection.AddNextMatchToMultiSelectionHandler" + commandId="org.eclipse.ui.edit.text.select.addNextMatchToMultiSelection"> + <enabledWhen> + <with + variable="activeEditor"> + <instanceof + value="org.eclipse.ui.texteditor.ITextEditor"> + </instanceof> + </with> + </enabledWhen> + </handler> + <handler + class="org.eclipse.ui.internal.texteditor.multiselection.RemoveLastMatchFromMultiSelectionHandler" + commandId="org.eclipse.ui.edit.text.select.removeLastMatchFromMultiSelection"> + <enabledWhen> + <with + variable="activeEditor"> + <instanceof + value="org.eclipse.ui.texteditor.ITextEditor"> + </instanceof> + </with> + </enabledWhen> + </handler> + <handler + class="org.eclipse.ui.internal.texteditor.multiselection.StopMultiSelectionHandler" + commandId="org.eclipse.ui.edit.text.select.stopMultiSelection"> + <enabledWhen> + <with + variable="activeEditor"> + <instanceof + value="org.eclipse.ui.texteditor.ITextEditor"> + </instanceof> + </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 new file mode 100644 index 00000000000..f54d153cf7b --- /dev/null +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AbstractMultiSelectionHandler.java @@ -0,0 +1,294 @@ +/******************************************************************************* + * 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.widgets.Control; + +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; + +import org.eclipse.jface.viewers.ISelection; + +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.MultiTextSelection; +import org.eclipse.jface.text.Region; + +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.handlers.HandlerUtil; + +import org.eclipse.ui.texteditor.ITextEditor; +import org.eclipse.ui.texteditor.ITextEditorExtension5; + +/** + * Common super class for Multi-Selection-actions, containing various helper + * methods. Subclasses need to overwrite {@link #execute()}, which is only + * invoked if the {@link #textEditor} and {@link #document} could be properly + * initialized. + * + * @see AddAllMatchesToMultiSelectionHandler + * @see AddNextMatchToMultiSelectionHandler + * @see RemoveLastMatchFromMultiSelectionHandler + * @see StopMultiSelectionHandler + */ +abstract class AbstractMultiSelectionHandler extends AbstractHandler { + private ExecutionEvent event; + private ITextEditor textEditor; + private IDocument document; + + /** + * This method needs to be overwritten from subclasses to handle the event. + * + * @throws ExecutionException + */ + public abstract void execute() throws ExecutionException; + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + if (initFrom(event)) { + execute(); + } + return null; + } + + public ExecutionEvent getEvent() { + return event; + } + + protected boolean isMultiSelectionActive() { + IRegion[] regions = getSelectedRegions(); + return regions != null && regions.length > 1; + } + + protected boolean nothingSelected() { + IRegion[] regions = getSelectedRegions(); + return regions == null || regions.length == 0 || regions[0].getLength() == 0; + } + + protected IRegion[] getSelectedRegions() { + ISelection selection = textEditor.getSelectionProvider().getSelection(); + + if (!(selection instanceof IMultiTextSelection)) { + return null; + } + + return ((IMultiTextSelection) selection).getRegions(); + } + + protected IRegion offsetAsCaretRegion(int offset) { + return new Region(offset, 0); + } + + protected void selectRegion(IRegion region) throws ExecutionException { + selectRegions(new IRegion[] { region }); + } + + protected void selectRegions(IRegion[] regions) throws ExecutionException { + setBlockSelectionMode(false); + + ISelection newSelection = new MultiTextSelection(document, regions); + textEditor.getSelectionProvider().setSelection(newSelection); + } + + protected void selectIdentifierUnderCaret() throws ExecutionException { + int offset = getCaretOffset(); + + Region identifierRegion = getIdentifierUnderCaretRegion(offset); + if (identifierRegion != null) + selectRegion(identifierRegion); + } + + protected boolean allRegionsHaveSameText() { + if (nothingSelected()) + return false; + return allRegionsHaveSameText(getSelectedRegions()); + } + + private boolean allRegionsHaveSameText(IRegion[] regions) { + if (regions == null || regions.length == 1) + return true; + + try { + return allRegionsHaveText(regions, regionAsString(regions[0])); + } catch (BadLocationException e) { + return false; + } + } + + private boolean allRegionsHaveText(IRegion[] regions, String text) throws BadLocationException { + for (IRegion iRegion : regions) { + if (!text.equals(regionAsString(iRegion))) { + return false; + } + } + return true; + } + + protected IRegion[] addRegion(IRegion[] regions, IRegion newRegion) { + if (newRegion != null) { + IRegion[] newRegions = Arrays.copyOf(regions, regions.length + 1); + newRegions[newRegions.length - 1] = newRegion; + return newRegions; + } else { + return regions; + } + } + + protected IRegion[] removeLastRegionButOne(IRegion[] regions) { + if (regions == null || regions.length == 0) + return null; + if (regions.length == 1) { + return regions; + } + + return Arrays.copyOf(regions, regions.length - 1); + } + + protected int getCaretOffset() { + return getWidget().getCaretOffset(); + } + + protected void setCaretOffset(int offset) { + getWidget().setCaretOffset(offset); + } + + 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()); + } catch (BadLocationException e) { + throw new ExecutionException("Internal error in findNextMatch", e); + } + } + + protected IRegion[] findAllMatches(IRegion region) throws ExecutionException { + try { + String fullText = getFullText(); + String searchString = getTextOfRegion(region); + List<IRegion> regions = findAllMatches(fullText, searchString); + return toArray(regions); + } catch (BadLocationException e) { + throw new ExecutionException("Internal error in findAllMatches", e); + } + } + + private List<IRegion> findAllMatches(String fullText, String searchString) { + List<IRegion> regions = new ArrayList<>(); + int length = searchString.length(); + int matchPos = 0; + while ((matchPos = fullText.indexOf(searchString, matchPos)) >= 0) { + regions.add(new Region(matchPos, length)); + matchPos += length; + } + return regions; + } + + private boolean initFrom(ExecutionEvent event) { + this.event = event; + textEditor = getTextEditor(event); + if (textEditor == null) + return false; + document = getDocument(); + return true; + } + + private ITextEditor getTextEditor(ExecutionEvent event) { + IEditorPart editor = HandlerUtil.getActiveEditor(event); + return editor instanceof ITextEditor ? (ITextEditor) editor : null; + } + + private IDocument getDocument() { + return textEditor.getDocumentProvider().getDocument(textEditor.getEditorInput()); + } + + private IRegion[] toArray(List<IRegion> regions) { + return regions.toArray(new IRegion[regions.size()]); + } + + private int offsetAfter(IRegion region) { + return region.getOffset() + region.getLength(); + } + + private String getTextOfRegion(IRegion region) throws BadLocationException { + return document.get(region.getOffset(), region.getLength()); + } + + private String getFullText() { + return document.get(); + } + + private String regionAsString(IRegion region) throws BadLocationException { + return document.get(region.getOffset(), region.getLength()); + } + + private Region getIdentifierUnderCaretRegion(int offset) { + try { + int startOffset = findStartOfIdentifier(offset); + int endOffset = findEndOfIdentifier(startOffset); + Region identifierRegion = new Region(startOffset, endOffset - startOffset); + return identifierRegion; + } catch (BadLocationException e) { + return null; + } + } + + private int findStartOfIdentifier(int offset) throws BadLocationException { + for (int i = offset - 1; i >= 0; i--) { + if (!isJavaIdentifierCharAtPos(i)) { + return i + 1; + } + } + return 0; // start of document reached + } + + private int findEndOfIdentifier(int offset) throws BadLocationException { + for (int i = offset; i <= document.getLength(); i++) { + if (i == document.getLength() || !isJavaIdentifierCharAtPos(i)) { + return i; + } + } + return offset; + } + + private boolean isJavaIdentifierCharAtPos(int i) throws BadLocationException { + return Character.isJavaIdentifierStart(document.getChar(i)) + || Character.isJavaIdentifierPart(document.getChar(i)); + } + + private StyledText getWidget() { + return (StyledText) textEditor.getAdapter(Control.class); + } + + private void setBlockSelectionMode(boolean blockSelectionMode) { + if (!(textEditor instanceof ITextEditorExtension5)) { + return; + } + ITextEditorExtension5 ext = (ITextEditorExtension5) textEditor; + ext.setBlockSelectionMode(blockSelectionMode); + } +} 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 new file mode 100644 index 00000000000..b9d09fc7cf1 --- /dev/null +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddAllMatchesToMultiSelectionHandler.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2022 Dirk Steinkamp + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Dirk Steinkamp <dirk.steinkamp@gmx.de> - initial API and implementation + *******************************************************************************/ + package org.eclipse.ui.internal.texteditor.multiselection; + +import org.eclipse.core.commands.ExecutionException; + +import org.eclipse.jface.text.IRegion; + +/** + * Handler to extend the current selection to all found matches in the document. + * If nothing is selected, an implicit selection of the word under the cursor is + * performed and the selection performed with this. + */ +public class AddAllMatchesToMultiSelectionHandler extends AbstractMultiSelectionHandler { + + @Override + public void execute() throws ExecutionException { + if (nothingSelected()) { + selectIdentifierUnderCaret(); + } + extendSelectionToAllMatches(); + } + + private void extendSelectionToAllMatches() throws ExecutionException { + if (allRegionsHaveSameText()) { + IRegion[] regions = getSelectedRegions(); + selectRegions(findAllMatches(regions[0])); + } + } +} diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddNextMatchToMultiSelectionHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddNextMatchToMultiSelectionHandler.java new file mode 100644 index 00000000000..16f9d479d40 --- /dev/null +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/AddNextMatchToMultiSelectionHandler.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2022 Dirk Steinkamp + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Dirk Steinkamp <dirk.steinkamp@gmx.de> - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.texteditor.multiselection; + +import org.eclipse.core.commands.ExecutionException; + +import org.eclipse.jface.text.IRegion; + +/** + * Handler to extend the current selection to the next found match below the + * selection. If no word is selected, an implicit selection of the word under + * the cursor is performed. + */ +public class AddNextMatchToMultiSelectionHandler extends AbstractMultiSelectionHandler { + + @Override + public void execute() throws ExecutionException { + if (nothingSelected()) { + selectIdentifierUnderCaret(); + } else { + extendSelectionToNextMatch(); + } + } + + private void extendSelectionToNextMatch() throws ExecutionException { + if (allRegionsHaveSameText()) { + IRegion[] regions = getSelectedRegions(); + IRegion nextMatch = findNextMatch(regions[regions.length - 1]); + selectRegions(addRegion(regions, nextMatch)); + } + } +} diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/RemoveLastMatchFromMultiSelectionHandler.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/RemoveLastMatchFromMultiSelectionHandler.java new file mode 100644 index 00000000000..35be36dd579 --- /dev/null +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/RemoveLastMatchFromMultiSelectionHandler.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * 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 new file mode 100644 index 00000000000..fc28e7c922e --- /dev/null +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/multiselection/StopMultiSelectionHandler.java @@ -0,0 +1,37 @@ +/******************************************************************************* + * 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; + +/** + * Handler to stop multi-selection mode. If more than one selection is active, + * all selections are revoked, and the caret is positioned at position of the + * first caret. + */ +public class StopMultiSelectionHandler extends AbstractMultiSelectionHandler { + + @Override + public void execute() throws ExecutionException { + if (isMultiSelectionActive()) { + stopMultiSelection(); + } + } + + private void stopMultiSelection() throws ExecutionException { + int caretOffset = getCaretOffset(); + selectRegion(offsetAsCaretRegion(caretOffset)); + setCaretOffset(caretOffset); + } +} |