diff options
21 files changed, 1252 insertions, 89 deletions
diff --git a/org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos.png b/org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos.png Binary files differnew file mode 100644 index 00000000000..995f1badd96 --- /dev/null +++ b/org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos.png diff --git a/org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos@2x.png b/org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos@2x.png Binary files differnew file mode 100644 index 00000000000..ee5c0432839 --- /dev/null +++ b/org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos@2x.png diff --git a/org.eclipse.ui.editors/icons/full/etool16/next_edit_pos.png b/org.eclipse.ui.editors/icons/full/etool16/next_edit_pos.png Binary files differnew file mode 100644 index 00000000000..5fa266b0760 --- /dev/null +++ b/org.eclipse.ui.editors/icons/full/etool16/next_edit_pos.png diff --git a/org.eclipse.ui.editors/icons/full/etool16/next_edit_pos@2x.png b/org.eclipse.ui.editors/icons/full/etool16/next_edit_pos@2x.png Binary files differnew file mode 100644 index 00000000000..47c1f8a8159 --- /dev/null +++ b/org.eclipse.ui.editors/icons/full/etool16/next_edit_pos@2x.png diff --git a/org.eclipse.ui.editors/plugin.properties b/org.eclipse.ui.editors/plugin.properties index 42b9b6ea423..f21d49ba5e3 100644 --- a/org.eclipse.ui.editors/plugin.properties +++ b/org.eclipse.ui.editors/plugin.properties @@ -84,6 +84,8 @@ revisionInfo.label= Revision Information goToLastEditPosition.label= Last Edit Lo&cation goToLastEditPosition.tooltip= Last Edit Location +goToNextEditPosition.label= Next Edit Lo&cation +goToNextEditPosition.tooltip= Next Edit Location textEditorNavigationActionSet.label= Editor Navigation diff --git a/org.eclipse.ui.editors/plugin.xml b/org.eclipse.ui.editors/plugin.xml index cf169c3895f..050a3d0f3d3 100644 --- a/org.eclipse.ui.editors/plugin.xml +++ b/org.eclipse.ui.editors/plugin.xml @@ -481,6 +481,19 @@ tooltip="%goToLastEditPosition.tooltip" initialEnabled="false"> </action> + <action + toolbarPath="org.eclipse.ui.workbench.navigate/history.group" + id="org.eclipse.ui.edit.text.gotoNextEditPosition" + class="org.eclipse.ui.texteditor.GotoNextEditPositionAction" + definitionId="org.eclipse.ui.edit.text.gotoNextEditPosition" + disabledIcon="$nl$/icons/full/dtool16/next_edit_pos.png" + icon="$nl$/icons/full/etool16/next_edit_pos.png" + helpContextId="org.eclipse.ui.goto_next_edit_position_action_context" + label="%goToNextEditPosition.label" + menubarPath="navigate/" + tooltip="%goToNextEditPosition.tooltip" + initialEnabled="false"> + </action> </actionSet> <actionSet label="%conversionActionSet.label" diff --git a/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/TextEditorPluginTest.java b/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/TextEditorPluginTest.java new file mode 100644 index 00000000000..57d2faea923 --- /dev/null +++ b/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/TextEditorPluginTest.java @@ -0,0 +1,387 @@ +/******************************************************************************* + * Copyright (c) 2000, 2016 IBM Corporation and others. + * + * 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: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.workbench.texteditor.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Random; + +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runners.MethodSorters; + +import org.eclipse.ui.internal.texteditor.HistoryTracker; + +/** + * Tests the FindReplaceDialog. + * + * @since 3.1 + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class TextEditorPluginTest { + + @Rule + public TestName testName = new TestName(); + + Random rand = new Random(55); //pseudo-random for repeatability + + @Test + public void testEditPositionHistory() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, true); + + assertEquals(0, history.getSize()); + history.addOrReplace(10); + assertEquals(1, history.getSize()); + assertTrue(10 == history.getCurrentBrowsePoint()); + + history.addOrReplace(20); + assertEquals(2, history.getSize()); + assertTrue(20 == history.getCurrentBrowsePoint()); + + history.addOrReplace(30); + assertEquals(3, history.getSize()); + assertTrue(30 == history.getCurrentBrowsePoint()); + + checkContent(history, new Integer[] { 10, 20, 30 }); + + + int replaced = history.addOrReplace(40); + assertEquals(3, history.getSize()); + assertEquals(10, replaced); + assertEquals(Integer.valueOf(40), history.getCurrentBrowsePoint()); + + HistoryTracker.Navigator<Integer> nav = history.navigator(); + assertEquals(Integer.valueOf(40), nav.currentItem()); + assertEquals(Integer.valueOf(30), nav.priorItem()); + assertEquals(Integer.valueOf(20), nav.priorItem()); + assertEquals(Integer.valueOf(40), nav.priorItem()); + checkContent(history, new Integer[] { 20, 30, 40 }); + + assertFalse(history.contains(10)); + + replaced = history.addOrReplace(22); + assertTrue(history.contains(22)); + assertFalse(history.contains(20)); + assertEquals(20, replaced); + checkContent(history, new Integer[] { 30, 40, 22 }); + //assertTrue(22 == history.getCurrentBrowsePoint()); + + replaced = history.addOrReplace(31); + assertTrue(history.contains(31)); + assertFalse(history.contains(30)); + assertEquals(30, replaced); + assertTrue(31 == history.getCurrentBrowsePoint()); + checkContent(history, new Integer[] { 40, 22, 31 }); + + replaced = history.addOrReplace(60); + assertTrue(history.contains(60)); + assertTrue(60 == history.getCurrentBrowsePoint()); + assertEquals(3, history.getSize()); + checkContent(history, new Integer[] { 22, 31, 60 }); + + assertTrue(31 == history.browseBackward()); + assertTrue(31 == history.getCurrentBrowsePoint()); + assertEquals(3, history.getSize()); + + //consuming size times should bring you full circle back to origin + testBacktrackCycle(history); + + //try editing after backtracking less than full cycle + history.browseBackward(); + Integer last = history.getCurrentBrowsePoint(); + assertEquals(Integer.valueOf(22), last); + history.addOrReplace(11); + checkContent(history, new Integer[] { 31, 60, 11 }); + + testBacktrackCycle(history); + history.browseBackward(); + assertEquals(Integer.valueOf(60), history.getCurrentBrowsePoint()); + } + + <T> void checkContent(HistoryTracker<T> history, T[] data) { + HistoryTracker.Navigator<T> nav = history.navigator(); + assertEquals(data[data.length - 1], nav.currentItem()); + + for(int i=data.length - 2; i>0; i--) { + assertEquals(data[i], nav.priorItem()); + } + assertEquals(history.getSize(), data.length); + } + + @Test + public void testEditPositionHistory2() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, true); + + assertEquals(0, history.getSize()); + history.addOrReplace(10); + assertEquals(1, history.getSize()); + assertTrue(10 == history.getCurrentBrowsePoint()); + + history.addOrReplace(20); + assertEquals(2, history.getSize()); + assertTrue(20 == history.getCurrentBrowsePoint()); + + history.addOrReplace(30); + assertEquals(3, history.getSize()); + assertTrue(30 == history.getCurrentBrowsePoint()); + + int replaced = history.addOrReplace(22); + assertEquals(3, history.getSize()); + assertEquals(20, replaced); + assertEquals(Integer.valueOf(22), history.getCurrentBrowsePoint()); + + assertEquals(Integer.valueOf(30), history.browseBackward()); + assertEquals(Integer.valueOf(10), history.browseBackward()); + assertEquals(Integer.valueOf(22), history.browseBackward()); + + testBacktrackCycle(history); + } + + @Test + public void testHistoryEviction() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, + true); + + assertEquals(0, history.getSize()); + history.addOrReplace(10); + assertEquals(1, history.getSize()); + assertTrue(10 == history.getCurrentBrowsePoint()); + + history.addOrReplace(10); + assertEquals(1, history.getSize()); + assertTrue(10 == history.getCurrentBrowsePoint()); + + history.addOrReplace(11); + assertEquals(1, history.getSize()); + assertTrue(11 == history.getCurrentBrowsePoint()); + } + + @Test + public void testHistoryEviction2() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, + true); + + history.addOrReplace(10); + history.addOrReplace(20); + history.addOrReplace(30); + assertEquals(Integer.valueOf(20), history.browseBackward()); + assertEquals(Integer.valueOf(10), history.addOrReplace(40)); + } + + @Test + public void testHistoryEviction3() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, + true); + + history.addOrReplace(10); + history.addOrReplace(11); + history.addOrReplace(20); + history.addOrReplace(21); + history.addOrReplace(12); + checkContent(history, new Integer[] { 21, 12 }); + } + + @Test + public void testLinearEditPositionHistory() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, + false); + + assertEquals(0, history.getSize()); + history.addOrReplace(10); + assertEquals(1, history.getSize()); + assertTrue(10 == history.getCurrentBrowsePoint()); + + history.addOrReplace(20); + assertEquals(2, history.getSize()); + assertTrue(20 == history.getCurrentBrowsePoint()); + + history.addOrReplace(30); + assertEquals(3, history.getSize()); + assertTrue(30 == history.getCurrentBrowsePoint()); + + int replaced = history.addOrReplace(22); + assertEquals(3, history.getSize()); + assertEquals(20, replaced); + assertEquals(Integer.valueOf(22), history.getCurrentBrowsePoint()); + + assertEquals(Integer.valueOf(30), history.browseBackward()); + assertEquals(Integer.valueOf(10), history.browseBackward()); + assertEquals(Integer.valueOf(10), history.browseBackward()); + + assertEquals(Integer.valueOf(30), history.browseForward()); + assertEquals(Integer.valueOf(22), history.browseForward()); + assertEquals(Integer.valueOf(22), history.browseForward()); + + } + + @Test + public void testLinearEditPositionHistory2() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, + false); + + history.addOrReplace(10); + history.addOrReplace(20); + history.addOrReplace(30); + + int replaced = history.addOrReplace(22); + assertEquals(3, history.getSize()); + assertEquals(20, replaced); + assertEquals(Integer.valueOf(22), history.getCurrentBrowsePoint()); + + //end reached, go no further + assertEquals(Integer.valueOf(22), history.getNext()); + + assertEquals(Integer.valueOf(30), history.browseBackward()); + assertEquals(Integer.valueOf(10), history.browseBackward()); + + //beginning reached, go no further + assertEquals(Integer.valueOf(10), history.browseBackward()); + } + + @Test + public void testMRUOrderAlwaysPreserved() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, + false); + + history.addOrReplace(10); + history.addOrReplace(20); + history.addOrReplace(30); + + assertEquals(Integer.valueOf(20), history.browseBackward()); + history.addOrReplace(11); + assertEquals(Integer.valueOf(11), history.getCurrentBrowsePoint()); + + assertEquals(Integer.valueOf(30), history.browseBackward()); + assertEquals(Integer.valueOf(20), history.browseBackward()); + } + + @Test + public void testMRUOrderAlwaysPreserved2() { + HistoryTracker<Integer> history= new HistoryTracker<>(3, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, + false); + + history.addOrReplace(10); + history.addOrReplace(20); + history.addOrReplace(30); + + assertEquals(Integer.valueOf(20), history.browseBackward()); + history.addOrReplace(40); + assertEquals(Integer.valueOf(40), history.getCurrentBrowsePoint()); + + assertEquals(Integer.valueOf(30), history.browseBackward()); + assertEquals(Integer.valueOf(20), history.browseBackward()); + assertEquals(Integer.valueOf(20), history.browseBackward()); + + } + + private <T> void testBacktrackCycle(HistoryTracker<T> history) { + T last = history.getCurrentBrowsePoint(); + for(int i=0; i<history.getSize() -1; i++) { + history.browseBackward(); + assertNotEquals(last, history.getCurrentBrowsePoint()); + } + history.browseBackward(); + assertEquals(last, history.getCurrentBrowsePoint()); + } + + @Test + public void testEditPositionHistoryChaos() { + final int HISTORY_SIZE= 10; + HistoryTracker<Integer> history= new HistoryTracker<>(HISTORY_SIZE, + Integer.class, + (a, b) -> Math.abs(a - b) < 5, true); + + for(int i=0;i<100;i++) { + if(rand.nextBoolean()) { + addRandom(history); + } else { + goBack(history); + } + assertTrue(history.isHealthy()); + assertTrue(history.getSize() <= HISTORY_SIZE); + } + } + + @Test + public void testLinearEditPositionHistoryChaos() { + final int HISTORY_SIZE= 10; + HistoryTracker<Integer> history= new HistoryTracker<>(HISTORY_SIZE, + Integer.class, + (a,b) -> Math.abs(a - b) < 5, + false + ); + + int backsInARow = 0; + for(int i=0;i<100;i++) { + if(rand.nextBoolean()) { + backsInARow = 0; + addRandom(history); + } else { + backsInARow ++; + goBackLinear(history, backsInARow < history.getSize()); + } + assertTrue(history.isHealthy()); + assertTrue(history.getSize() <= HISTORY_SIZE); + } + } + + + private void addRandom(HistoryTracker<Integer> history) { + Integer latest = rand.nextInt(50); + history.addOrReplace(latest); + assertEquals(latest, history.getCurrentBrowsePoint()); + } + + private void goBack(HistoryTracker<Integer> history) { + int size = history.getSize(); + Integer latest = history.getCurrentBrowsePoint(); + Integer prior = history.browseBackward(); + if (size > 1) { + assertNotEquals(latest, prior); + } + } + + private void goBackLinear(HistoryTracker<Integer> history, boolean shouldMove) { + Integer latest= history.getCurrentBrowsePoint(); + Integer prior = history.browseBackward(); + if(shouldMove) + assertNotEquals(latest, prior); + else + assertEquals(latest, prior); + } + +} diff --git a/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java b/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java index db70c7a3560..c6f03ab0862 100644 --- a/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java +++ b/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java @@ -42,7 +42,8 @@ import org.eclipse.ui.workbench.texteditor.tests.rulers.RulerTestSuite; AbstractTextZoomHandlerTest.class, DocumentLineDifferTest.class, MinimapPageTest.class, - MinimapWidgetTest.class + MinimapWidgetTest.class, + TextEditorPluginTest.class }) public class WorkbenchTextEditorTestSuite { // see @SuiteClasses diff --git a/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF b/org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF index 67ed9a92f96..44dced6b1cc 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.14.300.qualifier +Bundle-Version: 3.15.0.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 a47dc433f86..fea5ab02b1a 100644 --- a/org.eclipse.ui.workbench.texteditor/plugin.properties +++ b/org.eclipse.ui.workbench.texteditor/plugin.properties @@ -67,6 +67,8 @@ lowerCase.label= To Lower Case goToLastEditPosition.label= Last Edit Location goToLastEditPosition.description= Last edit location +goToNextEditPosition.label= Next Edit Location +goToNextEditPosition.description= Next edit location command.clearMark.description = Clear the mark command.clearMark.name = Clear Mark diff --git a/org.eclipse.ui.workbench.texteditor/plugin.xml b/org.eclipse.ui.workbench.texteditor/plugin.xml index 26cf0ac9ffd..093dffa0316 100644 --- a/org.eclipse.ui.workbench.texteditor/plugin.xml +++ b/org.eclipse.ui.workbench.texteditor/plugin.xml @@ -359,6 +359,12 @@ id="org.eclipse.ui.edit.text.gotoLastEditPosition"> </command> <command + name="%goToNextEditPosition.label" + description="%goToNextEditPosition.description" + categoryId="org.eclipse.ui.category.navigate" + id="org.eclipse.ui.edit.text.gotoNextEditPosition"> + </command> + <command name="%smartEnter.label" description="%smartEnter.description" categoryId="org.eclipse.ui.category.textEditor" @@ -593,6 +599,11 @@ schemeId="org.eclipse.ui.defaultAcceleratorConfiguration" sequence="CTRL+Q"/> <!-- Command+Q is quit on carbon, so don't overwrite it --> <key + commandId="org.eclipse.ui.edit.text.gotoNextEditPosition" + contextId="org.eclipse.ui.contexts.window" + schemeId="org.eclipse.ui.defaultAcceleratorConfiguration" + sequence="M3+CTRL+Q"/> + <key commandId="org.eclipse.ui.edit.text.smartEnter" contextId="org.eclipse.ui.textEditorScope" sequence="M2+CR" diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditPosition.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditPosition.java index 87805f2d0fe..c59ffc83611 100644 --- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditPosition.java +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditPosition.java @@ -32,6 +32,12 @@ public final class EditPosition { private final String fEditorId; /** The position */ private final Position fPosition; + /** + * how many characters may come in between two edit positions for them to + * still be lumped into same bucket in position history (designed to prevent + * filling history with meaningless noise of very similar positions) + */ + public static final int PROXIMITY_THRESHOLD = 30; /** * Creates a new edit position. @@ -73,4 +79,47 @@ public final class EditPosition { public Position getPosition() { return fPosition; } + + /** + * @param a Position to compare the other arg against + * @param b Another position to compare the first arg against + * @param threshold The maximum allowed distance between Position args for them + * to be considered co-located + * @return true if both Position args are colocated as defined by the threshold + * param + * @since 3.15 + */ + public static boolean areCoLocated(Position a, Position b, int threshold) { + if (a == null || b == null) { + return false; + } + int center1 = a.offset + (a.length / 2); + int center2 = b.offset + (b.length / 2); + int centerDistance = Math.abs(center1 - center2); + int minWithoutOverlap = a.length / 2 + b.length / 2; + return centerDistance < (minWithoutOverlap + threshold); + } + + /** + * @since 3.15 + */ + public static boolean areCoLocated(Position a, Position b) { + return EditPosition.areCoLocated(a, b, EditPosition.PROXIMITY_THRESHOLD); + } + + /** + * @since 3.15 + */ + public static boolean areCoLocated(EditPosition a, EditPosition b, int threshold) { + return a != null && b != null && a.getEditorInput().getName() + .equals(b.getEditorInput().getName()) + && EditPosition.areCoLocated(a.getPosition(), b.getPosition(), threshold); + } + + /** + * @since 3.15 + */ + public static boolean areCoLocated(EditPosition a, EditPosition b) { + return EditPosition.areCoLocated(a, b, EditPosition.PROXIMITY_THRESHOLD); + } } diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.java index f3743d6b667..311a154b4ad 100644 --- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.java +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.java @@ -28,6 +28,8 @@ final class EditorMessages extends NLS { public static String Editor_error_gotoLastEditPosition_title; public static String Editor_error_gotoLastEditPosition_message; + public static String Editor_error_gotoNextEditPosition_title; + public static String Editor_error_gotoNextEditPosition_message; static { NLS.initializeMessages(BUNDLE_NAME, EditorMessages.class); diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.properties b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.properties index edf80884612..9603efa20a5 100644 --- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.properties +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.properties @@ -17,3 +17,5 @@ Editor_error_gotoLastEditPosition_title= Problems going to last edit position Editor_error_gotoLastEditPosition_message= Unable to go to the last edit position. +Editor_error_gotoNextEditPosition_title= Problems going to next edit position +Editor_error_gotoNextEditPosition_message= Unable to go to the next edit position. diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/HistoryTracker.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/HistoryTracker.java new file mode 100644 index 00000000000..a96686c6371 --- /dev/null +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/HistoryTracker.java @@ -0,0 +1,412 @@ +/******************************************************************************* +* Copyright (c) 2020 Ari Kast and others. +* +* 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: +* Ari Kast - initial API and implementation +*******************************************************************************/ + +package org.eclipse.ui.internal.texteditor; + +import java.lang.reflect.Array; + +/** + * @author Ari Kast + * + * Tracks history in order of insertion. It can operate either as a ring + * or as a line: in ring mode, if history size is N, then calling + * goBackward N times brings you full circle back to your current + * location in linear mode, if history size is N, then calling + * goBackward N times brings you to the beginning, after which + * additional calls to goBackward will have no effect until either + * goForward is called or a new entry is added Both linear and ring mode + * overwrite history as needed when buffer is full + * @param <T> the type of the object instances being tracked in history + * + * @since 3.15 + */ +public class HistoryTracker<T> { + // the actual historical data + T[] fHistory; + + // function to determine whether history elements can be merged + CandidateEvaluator<T> fEvaluator; + + // pointer to current location in history queue + Navigator<T> fBrowsePoint; + + // pointer to most recent insertion to history queue. New insertions always + // go to index fMostRecent + 1 + Navigator<T> fInsertionPoint; + + // the size of the dataset contained in history queue + // grows til it reaches history.length, and generally wont + // shrink + int fSize; + + // controls whether navigation wraps around in circular fashion + // or if it is purely linear + boolean fUseCircularNavigation; + + /** + * @param historySize the buffer size. additional insertions will + * overwrite oldest insertions + * @param clazz the class type of the objects being tracked + * @param evaluator The expression which compares incoming elements + * against existing elements. If this expression + * yields true, then the element(s) for which it + * was true is/are replaced + * @param useCircularNavigation when true the history operates in ring mode, + * otherwise it is linear + */ + @SuppressWarnings("unchecked") + public HistoryTracker(int historySize, Class<T> clazz, CandidateEvaluator<T> evaluator, + boolean useCircularNavigation) { + historySize = Math.max(historySize, 1); // size < 1 makes no sense, so + // enforce at least size 1 + fHistory = (T[]) Array.newInstance(clazz, historySize); + this.fEvaluator = evaluator; + this.fUseCircularNavigation = useCircularNavigation; + fBrowsePoint = new Navigator<>(this); + fInsertionPoint = new Navigator<>(this); + } + + public T browseBackward() { + if (canGoBackward()) { + fBrowsePoint.decr(); + } + return getCurrentBrowsePoint(); + } + + public T browseForward() { + if (canGoForward()) { + fBrowsePoint.incr(); + } + return getCurrentBrowsePoint(); + } + + private boolean canGoBackward() { + return fSize > 0 && (fUseCircularNavigation || fBrowsePoint.getPriorIndex() != fInsertionPoint.getIndex()); + } + + private boolean canGoForward() { + return fSize > 0 && (fUseCircularNavigation || fBrowsePoint.getIndex() != fInsertionPoint.getIndex()); + } + + public T getCurrentBrowsePoint() { + return fBrowsePoint.currentItem(); + } + + public T getNext() { + if (canGoForward()) { + return getAt(fBrowsePoint.getNextIndex()); + } else { + return fBrowsePoint.currentItem(); + } + } + + T getAt(int index) { + if (fSize == 0) { + return null; + } + + int i = moddedIndex(index); + return fHistory[i]; + } + + /** + * This method always adds the parameter element at the current history + * location. If history is full (capacity == size) then an existing element is + * overwritten in the process. If there exists an element such that + * evaluator.canReplace() yields true, then that element is prioritized for + * being overwritten + * + * @param newItem The object instance being added to history + * @return The element which was evicted to make room for the incoming element. + * Returns null if nothing was evicted + */ + public T addOrReplace(T newItem) { + + T answer = null; + // if a replacement candidate exists, delete it since this + // incoming will be replacing it + + /** + * This loop could potentially degrade to N^2 performance in case of multiple + * deletions, but in practice it 1) shouldnt matter for our small history sizes, + * and 2) will seldom delete more than 1 item for O(N) performance. + * + * If performance is ever a concern, several improvements could be made: 1) + * perform all deletions in a single pass, then do a single additional pass for + * compaction for a total of 2 passes 2) uncomment the "break" statement as + * explained below 3) use a different data structure altogether, eg maybe an + * ordered tree of some kind + */ + for (int i = fInsertionPoint.getIndex(); i > fInsertionPoint.getIndex() - fSize; i--) { + T candidate = getAt(i); + if (candidate != null && fEvaluator.canReplace(newItem, candidate)) { + answer = deleteAt(i); + /** + * if performance is ever a concern, the below break could be uncommented so + * that this method only de-dupes first match instead of all matches. generally + * first match would be sufficient, except there can be drift over time eg if + * history contains [10,20,30] and vicinity threshold defined as distance 2, + * then you insert the following series: 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 + * even if each insert overwrote the prior, you still could end up with dataset + * [20, 20, 30]. By not breaking here we prevent that state from occuring, but + * at some computation cost. Therefore if cost (performance) ever a problem, we + * could just accept that occasional suboptimal history state for better + * performance + */ + // break; + } + } + + T replaced = addLast(newItem); + if (answer == null) { + answer = replaced; + } + + return answer; + } + + private T addLast(T newItem) { + if (newItem == null) { + return deleteLast(); + } + + if (fSize >= fHistory.length) { + // no space, so just overwrite next slot + fInsertionPoint.incr(); + fBrowsePoint.jumpTo(fInsertionPoint); + return replaceAt(newItem, fInsertionPoint); + } else { + // there's at least one empty slot so data size can grow + expand(); + fInsertionPoint.incr(); + fBrowsePoint.jumpTo(fInsertionPoint); + return shiftInsert(newItem); + } + + } + + // inserts here and shifts existing values until either empty space is found + // or end is reached, in which case last evaluated slot is discarded + private T shiftInsert(T newItem) { + + T answer = replaceAt(newItem, fInsertionPoint); + T tmp = answer; + + // shift from here to the end until empty slot found + for (int i = fInsertionPoint.getIndex() + 1; i < fSize; i++) { + tmp = replaceAt(tmp, i); + } + + return answer; + } + + // deletes and shifts to fill in the hole created by deletion + private T deleteAt(int index) { + if (fSize == 0) { + return null; + } + int modIndex = moddedIndex(index); + T answer = replaceAt(null, modIndex); + + // shift to fill in any gaps to keep data contiguous so that mod + // calculations work + T priorVal = null; + for (int i = fSize - 1; i >= modIndex; i--) { + priorVal = replaceAt(priorVal, i); + } + + if (answer != null) { + fSize--; + } + + // adjust insertion point if it was affected by the shift + if (fInsertionPoint.getIndex() >= modIndex && fSize > 0) { + fInsertionPoint.decr(); + } + + // adjust browse point if it was affected by the shift + if (fBrowsePoint.getIndex() >= modIndex && fSize > 0) { + fBrowsePoint.decr(); + } + + return answer; + } + + public T deleteLast() { + T answer = deleteAt(fInsertionPoint.getIndex()); + return answer; + } + + private T replaceAt(T newItem, int index) { + if (fSize == 0) { + return null; + } + int i = moddedIndex(index); + + T replaced = getAt(i); + fHistory[i] = newItem; + return replaced; + } + + private T replaceAt(T newItem, Navigator<T> navigator) { + return replaceAt(newItem, navigator.getIndex()); + } + + public T replaceLast(T newItem) { + if (newItem == null) { + return deleteLast(); + } + fBrowsePoint.jumpTo(fInsertionPoint); + return replaceAt(newItem, fInsertionPoint); + } + + void expand() { + fSize = Math.max(fSize, (fSize + 1) % (fHistory.length + 1)); + } + + private int moddedIndex(int index) { + return Math.floorMod(index, fSize); + } + + public boolean isEmpty() { + return fInsertionPoint.currentItem() == null; + } + + public int getSize() { + return fSize; + } + + public Navigator<T> navigator() { + Navigator<T> answer = new Navigator<>(this); + answer.jumpTo(fInsertionPoint); + return answer; + } + + // for internal use, not public + Navigator<T> navigator(int index) { + Navigator<T> answer = new Navigator<>(this); + answer.jumpTo(index); + return answer; + } + + /** + * This method is intended for testing/troubleshooting, not for general use + * Beware it has O(N) performance + * + * @param item The item to check whether this history contains + * @return true if history contains item + */ + public boolean contains(T item) { + if (item == null) { + return false; + } + + for (int i = 0; i < fSize; i++) { + if (item.equals(getAt(i))) { + return true; + } + } + return false; + } + + /** + * This method is intended for testing/troubleshooting + * + * @return true if the state of this object is healthy/as expected + */ + public boolean isHealthy() { + // make sure nulls are consolidated not scattered + boolean priorWasNull = false; + int flipCount = 0; + for (int i = 0; i < fHistory.length; i++) { + boolean isNull = (fHistory[i] == null); + if (priorWasNull != isNull) { + flipCount++; + priorWasNull = isNull; + } + } + return flipCount < 2; + } + + /** + * + * used during history compaction to consolidate like candidates in the history + */ + public static interface CandidateEvaluator<T> { + public boolean canReplace(T a, T b); + } + + /** + * for easy traversing thru history + */ + public static class Navigator<T> { + HistoryTracker<T> historyTracker; + int fIndex; + + public Navigator(HistoryTracker<T> tracker) { + this.historyTracker = tracker; + fIndex = Math.floorMod(-1, tracker.fHistory.length); + } + + void incr() { + if (historyTracker.fSize == 0) { + return; + } + + fIndex = getNextIndex(); + } + + void decr() { + if (historyTracker.fSize == 0) { + return; + } + + fIndex = getPriorIndex(); + } + + int getIndex() { + return fIndex; + } + + int getNextIndex() { + return historyTracker.moddedIndex(fIndex + 1); + } + + int getPriorIndex() { + return historyTracker.moddedIndex(fIndex - 1); + } + + public T currentItem() { + return historyTracker.getAt(fIndex); + } + + public T nextItem() { + incr(); + return historyTracker.getAt(fIndex); + } + + public T priorItem() { + decr(); + return historyTracker.getAt(fIndex); + } + + void jumpTo(Navigator<T> b) { + this.fIndex = b.fIndex; + } + + public void jumpTo(int index) { + fIndex = historyTracker.moddedIndex(index); + } + } +} diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/TextEditorPlugin.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/TextEditorPlugin.java index 1c2663c82a0..b999befd10d 100644 --- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/TextEditorPlugin.java +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/TextEditorPlugin.java @@ -47,13 +47,31 @@ import org.eclipse.ui.plugin.AbstractUIPlugin; */ public final class TextEditorPlugin extends AbstractUIPlugin implements IRegistryChangeListener { + /** how many edit locations will be remembered in history */ + public static final int EDIT_LOCATION_HISTORY_SIZE = 15; + /** The plug-in instance */ private static TextEditorPlugin fgPlugin; - /** The last edit position */ - private EditPosition fLastEditPosition; + /** + * tracks whether cursor has moved since fEditPositionHistory was used to + * return to prior location. If cursor has moved, then goto last edit + * location simply returns to last edit location. But if cursor has not + * moved, that means the command was invoked twice in a row without + * intervening other actions; in that case we start traversing backward thru + * history to prior edit locations + */ + boolean movedSinceLastEditRecall = true; + + // an ordered history of prior edit positions + private HistoryTracker<EditPosition> fEditPositionHistory = new HistoryTracker<>( + EDIT_LOCATION_HISTORY_SIZE, EditPosition.class, + (a, b) -> b.getPosition().isDeleted || b.getPosition().isDeleted + || EditPosition.areCoLocated(a, b), + false); + /** The action which goes to the last edit position */ - private Set<IAction> fLastEditPositionDependentActions; + private Set<IAction> fEditPositionDependentActions; /** * The quick diff extension registry. @@ -105,29 +123,53 @@ public final class TextEditorPlugin extends AbstractUIPlugin implements IRegistr */ public static final String REFERENCE_PROVIDER_EXTENSION_POINT= "quickDiffReferenceProvider"; //$NON-NLS-1$ + public boolean isMovedSinceLastEditRecall() { + return movedSinceLastEditRecall; + } + + public void setMovedSinceLastEditRecall(boolean movedSinceLastEditRecall) { + this.movedSinceLastEditRecall = movedSinceLastEditRecall; + } + + public HistoryTracker<EditPosition> getEditPositionHistory() { + return fEditPositionHistory; + } + + public void setEditPositionHistory(HistoryTracker<EditPosition> editPositionHistory) { + fEditPositionHistory = editPositionHistory; + } + /** * Returns the last edit position. * - * @return the last edit position or <code>null</code> if there is no last edit position + * @return the last edit position or <code>null</code> if there is no last + * edit position * @see EditPosition */ public EditPosition getLastEditPosition() { - return fLastEditPosition; + return fEditPositionHistory.getCurrentBrowsePoint(); } - /** - * Sets the last edit position. - * - * @param lastEditPosition the last edit position - * @see EditPosition - */ - public void setLastEditPosition(EditPosition lastEditPosition) { - fLastEditPosition= lastEditPosition; - if (fLastEditPosition != null && fLastEditPositionDependentActions != null) { - Iterator<IAction> iter= fLastEditPositionDependentActions.iterator(); + public EditPosition getNextEditPosition() { + return fEditPositionHistory.getNext(); + } + + public EditPosition backtrackEditPosition() { + return fEditPositionHistory.browseBackward(); + } + + public EditPosition advanceEditPosition() { + return fEditPositionHistory.browseForward(); + } + + public void enableLastEditPositionDependentActions() { + EditPosition last = fEditPositionHistory.getCurrentBrowsePoint(); + + if (last != null && getDependentActions() != null) { + Iterator<IAction> iter = getDependentActions().iterator(); while (iter.hasNext()) iter.next().setEnabled(true); - fLastEditPositionDependentActions= null; + setDependentActions(null); } } @@ -137,11 +179,11 @@ public final class TextEditorPlugin extends AbstractUIPlugin implements IRegistr * @param action the goto last edit position action */ public void addLastEditPositionDependentAction(IAction action) { - if (fLastEditPosition != null) + if (!fEditPositionHistory.isEmpty()) { return; - if (fLastEditPositionDependentActions == null) - fLastEditPositionDependentActions= new HashSet<>(); - fLastEditPositionDependentActions.add(action); + } + + addDependentAction(action); } /** @@ -150,12 +192,33 @@ public final class TextEditorPlugin extends AbstractUIPlugin implements IRegistr * @param action the action that depends on the last edit position */ public void removeLastEditPositionDependentAction(IAction action) { - if (fLastEditPosition != null) + if (!fEditPositionHistory.isEmpty()) { return; - if (fLastEditPositionDependentActions != null) - fLastEditPositionDependentActions.remove(action); + } + + removeDependentAction(action); } + private Set<IAction> getDependentActions() { + return fEditPositionDependentActions; + } + + private void setDependentActions(Set<IAction> actions) { + fEditPositionDependentActions = actions; + } + + public void addDependentAction(IAction action) { + if (getDependentActions() == null) { + setDependentActions(new HashSet<>()); + } + getDependentActions().add(action); + } + + public void removeDependentAction(IAction action) { + if (getDependentActions() != null) { + getDependentActions().remove(action); + } + } @Override public void start(BundleContext context) throws Exception { @@ -216,4 +279,5 @@ public final class TextEditorPlugin extends AbstractUIPlugin implements IRegistr public CodeMiningProviderRegistry getCodeMiningProviderRegistry() { return fCodeMiningProviderRegistry; } + } diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java index 93f30cf1fac..9835d97a60c 100644 --- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java @@ -215,6 +215,7 @@ import org.eclipse.ui.dialogs.PropertyDialogAction; import org.eclipse.ui.dnd.IDragAndDropService; import org.eclipse.ui.internal.texteditor.EditPosition; import org.eclipse.ui.internal.texteditor.FocusedInformationPresenter; +import org.eclipse.ui.internal.texteditor.HistoryTracker; import org.eclipse.ui.internal.texteditor.NLSUtility; import org.eclipse.ui.internal.texteditor.TextEditorPlugin; import org.eclipse.ui.internal.texteditor.rulers.StringSetSerializer; @@ -605,7 +606,6 @@ public abstract class AbstractTextEditor extends EditorPart implements ITextEdit * * @since 3.0 */ - private Position fLocalLastEditPosition; /** The posted updater code. */ private Runnable fRunnable = () -> { @@ -616,35 +616,36 @@ public abstract class AbstractTextEditor extends EditorPart implements ITextEdit // remember the last edit position if (isDirty() && fUpdateLastEditPosition) { + HistoryTracker<EditPosition> positionHistory = TextEditorPlugin.getDefault() + .getEditPositionHistory(); fUpdateLastEditPosition = false; ISelection sel = getSelectionProvider().getSelection(); IEditorInput input = getEditorInput(); IDocument document = getDocumentProvider().getDocument(input); - - if (fLocalLastEditPosition != null) { - if (document != null) { - document.removePosition(fLocalLastEditPosition); - } - fLocalLastEditPosition = null; - } + IEditorSite editorSite = getEditorSite(); + if (editorSite instanceof MultiPageEditorSite) + editorSite = ((MultiPageEditorSite) editorSite).getMultiPageEditor().getEditorSite(); if (sel instanceof ITextSelection && !sel.isEmpty()) { ITextSelection s = (ITextSelection) sel; - fLocalLastEditPosition = new Position(s.getOffset(), s.getLength()); + Position newPosition = new Position(s.getOffset(), s.getLength()); + EditPosition newEditPosition = new EditPosition(input, editorSite.getId(), newPosition); + EditPosition replaced = positionHistory.addOrReplace(newEditPosition); if (document != null) { + if (replaced != null) { + document.removePosition(replaced.getPosition()); + } try { - document.addPosition(fLocalLastEditPosition); + if (positionHistory.getSize() > 0) { + document.addPosition(positionHistory.getCurrentBrowsePoint().getPosition()); + } } catch (BadLocationException ex) { - fLocalLastEditPosition = null; + positionHistory.deleteLast(); } } } - IEditorSite editorSite = getEditorSite(); - if (editorSite instanceof MultiPageEditorSite) - editorSite = ((MultiPageEditorSite) editorSite).getMultiPageEditor().getEditorSite(); - TextEditorPlugin.getDefault() - .setLastEditPosition(new EditPosition(input, editorSite.getId(), fLocalLastEditPosition)); + TextEditorPlugin.getDefault().enableLastEditPositionDependentActions(); } } }; @@ -681,14 +682,25 @@ public abstract class AbstractTextEditor extends EditorPart implements ITextEdit @Override public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { - if (oldInput != null && fLocalLastEditPosition != null) { - oldInput.removePosition(fLocalLastEditPosition); - fLocalLastEditPosition= null; + HistoryTracker<EditPosition> positionHistory = TextEditorPlugin.getDefault().getEditPositionHistory(); + if (oldInput != null && !positionHistory.isEmpty()) { + for (int i = 0; i < positionHistory.getSize(); i++) + oldInput.removePosition(positionHistory.browseBackward().getPosition()); } } @Override public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { + discardHistoryFor(oldInput); + } + + private void discardHistoryFor(IDocument input) { + HistoryTracker<EditPosition> positionHistory = TextEditorPlugin.getDefault().getEditPositionHistory(); + if (input != null && !positionHistory.isEmpty()) { + for (int i = 0; i < positionHistory.getSize(); i++) + input.removePosition(positionHistory.browseBackward().getPosition()); + } + } } @@ -3073,7 +3085,7 @@ public abstract class AbstractTextEditor extends EditorPart implements ITextEdit private Runnable fRunnable = () -> { // check whether editor has not been disposed yet if (fSourceViewer != null && fSourceViewer.getDocument() != null) { - handleCursorPositionChanged(); + handleCursorPositionChangedWrapper(); updateSelectionDependentActions(); } }; @@ -3110,7 +3122,7 @@ public abstract class AbstractTextEditor extends EditorPart implements ITextEdit @Override public void keyPressed(KeyEvent e) { - handleCursorPositionChanged(); + handleCursorPositionChangedWrapper(); } @Override @@ -3127,7 +3139,7 @@ public abstract class AbstractTextEditor extends EditorPart implements ITextEdit @Override public void mouseUp(MouseEvent e) { - handleCursorPositionChanged(); + handleCursorPositionChangedWrapper(); } }; } @@ -6599,8 +6611,20 @@ public abstract class AbstractTextEditor extends EditorPart implements ITextEdit } /** - * Handles a potential change of the cursor position. - * Subclasses may extend. + * Prepares to handles a potential change of the cursor position. Wraps the + * actual handler which cannot itself be modified for fear of disrupting + * existing subclasses which may have overridden it without calling + * super.handleCursorPositionChanged() + * + * @since 3.15 + */ + private void handleCursorPositionChangedWrapper() { + TextEditorPlugin.getDefault().setMovedSinceLastEditRecall(true); + handleCursorPositionChanged(); + } + + /** + * Handles a potential change of the cursor position. Subclasses may extend. * * @since 2.0 */ diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoLastEditPositionAction.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoLastEditPositionAction.java index 2df3872a8ae..effdbea938b 100644 --- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoLastEditPositionAction.java +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoLastEditPositionAction.java @@ -69,50 +69,64 @@ public class GotoLastEditPositionAction extends Action implements IWorkbenchWind @Override public void run() { - EditPosition editPosition= TextEditorPlugin.getDefault().getLastEditPosition(); - if (editPosition == null) - return; - - final Position pos= editPosition.getPosition(); - if (pos == null || pos.isDeleted) - return; + if (!TextEditorPlugin.getDefault().isMovedSinceLastEditRecall()) { + TextEditorPlugin.getDefault().backtrackEditPosition(); + } + EditPosition editPosition = TextEditorPlugin.getDefault().getLastEditPosition(); + try { - IWorkbenchWindow window= getWindow(); - if (window == null) - return; + if (editPosition == null) { + return; + } - IWorkbenchPage page= window.getActivePage(); + final Position pos = editPosition.getPosition(); + if (pos == null || pos.isDeleted) { + return; + } - IEditorPart editor; - try { - editor= page.openEditor(editPosition.getEditorInput(), editPosition.getEditorId()); - } catch (PartInitException ex) { - IStatus status= new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID, IStatus.OK, "Go to Last Edit Location failed", ex); //$NON-NLS-1$ - TextEditorPlugin.getDefault().getLog().log(status); - return; - } + IWorkbenchWindow window = getWindow(); + if (window == null) { + return; + } - // Optimization - could also use else branch - if (editor instanceof ITextEditor) { - ITextEditor textEditor= (ITextEditor)editor; - textEditor.selectAndReveal(pos.offset, pos.length); - return; - } + IWorkbenchPage page = window.getActivePage(); - /* - * Workaround: send out a text selection - * XXX: Needs to be improved, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=32214 - */ - if (editor != null) { - IEditorSite site= editor.getEditorSite(); - if (site == null) + IEditorPart editor; + try { + editor = page.openEditor(editPosition.getEditorInput(), editPosition.getEditorId()); + } catch (PartInitException ex) { + IStatus status = new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID, IStatus.OK, + "Go to Last Edit Location failed", ex); //$NON-NLS-1$ + TextEditorPlugin.getDefault().getLog().log(status); return; + } - ISelectionProvider provider= editor.getEditorSite().getSelectionProvider(); - if (provider == null) + // Optimization - could also use else branch + if (editor instanceof ITextEditor) { + ITextEditor textEditor = (ITextEditor) editor; + textEditor.selectAndReveal(pos.offset, pos.length); return; - - provider.setSelection(new TextSelection(pos.offset, pos.length)); + } + + /* + * Workaround: send out a text selection XXX: Needs to be improved, + * see https://bugs.eclipse.org/bugs/show_bug.cgi?id=32214 + */ + if (editor != null) { + IEditorSite site = editor.getEditorSite(); + if (site == null) { + return; + } + + ISelectionProvider provider = editor.getEditorSite().getSelectionProvider(); + if (provider == null) { + return; + } + + provider.setSelection(new TextSelection(pos.offset, pos.length)); + } + } finally { + TextEditorPlugin.getDefault().setMovedSinceLastEditRecall(false); } } @@ -127,7 +141,7 @@ public class GotoLastEditPositionAction extends Action implements IWorkbenchWind // adding the same action twice has no effect. TextEditorPlugin.getDefault().addLastEditPositionDependentAction(action); // this is always the same action for this instance - fAction= action; + fAction = action; } } @@ -137,8 +151,9 @@ public class GotoLastEditPositionAction extends Action implements IWorkbenchWind * @return the workbench window */ private IWorkbenchWindow getWindow() { - if (fWindow == null) + if (fWindow == null) { fWindow= PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + } return fWindow; } diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoNextEditPositionAction.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoNextEditPositionAction.java new file mode 100644 index 00000000000..587a0644a3b --- /dev/null +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoNextEditPositionAction.java @@ -0,0 +1,162 @@ +/******************************************************************************* + * Copyright (c) 2020 IBM Corporation and others. + * + * 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: + * Ari Kast - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.texteditor; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionProvider; + +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.TextSelection; + +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IEditorSite; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.texteditor.EditPosition; +import org.eclipse.ui.internal.texteditor.TextEditorPlugin; + + +/** + * Goes to next edit position, ie travels forward in the edit position history + * Acts as a complement to GotoLastEditPositionAction which travels backward in + * the history. + * + * @since 3.15 + */ +public class GotoNextEditPositionAction extends Action implements IWorkbenchWindowActionDelegate { + + /** The workbench window */ + private IWorkbenchWindow fWindow; + /** The action */ + private IAction fAction; + + /** + * Creates a goto next edit action. + */ + public GotoNextEditPositionAction() { + PlatformUI.getWorkbench().getHelpSystem().setHelp(this, + IAbstractTextEditorHelpContextIds.GOTO_NEXT_EDIT_POSITION_ACTION); + setId(ITextEditorActionDefinitionIds.GOTO_NEXT_EDIT_POSITION); + setActionDefinitionId(ITextEditorActionDefinitionIds.GOTO_NEXT_EDIT_POSITION); + setEnabled(false); + } + + @Override + public void init(IWorkbenchWindow window) { + fWindow= window; + } + + @Override + public void run(IAction action) { + run(); + } + + @Override + public void run() { + if (!TextEditorPlugin.getDefault().isMovedSinceLastEditRecall()) { + TextEditorPlugin.getDefault().advanceEditPosition(); + } + EditPosition editPosition = TextEditorPlugin.getDefault().getNextEditPosition(); + try { + + if (editPosition == null) + return; + + final Position pos = editPosition.getPosition(); + if (pos == null || pos.isDeleted) + return; + + IWorkbenchWindow window = getWindow(); + if (window == null) + return; + + IWorkbenchPage page = window.getActivePage(); + + IEditorPart editor; + try { + editor = page.openEditor(editPosition.getEditorInput(), editPosition.getEditorId()); + } catch (PartInitException ex) { + IStatus status = new Status(IStatus.ERROR, TextEditorPlugin.PLUGIN_ID, IStatus.OK, + "Go to Last Edit Location failed", ex); //$NON-NLS-1$ + TextEditorPlugin.getDefault().getLog().log(status); + return; + } + + // Optimization - could also use else branch + if (editor instanceof ITextEditor) { + ITextEditor textEditor = (ITextEditor) editor; + textEditor.selectAndReveal(pos.offset, pos.length); + return; + } + + /* + * Workaround: send out a text selection XXX: Needs to be improved, + * see https://bugs.eclipse.org/bugs/show_bug.cgi?id=32214 + */ + if (editor != null) { + IEditorSite site = editor.getEditorSite(); + if (site == null) + return; + + ISelectionProvider provider = editor.getEditorSite().getSelectionProvider(); + if (provider == null) + return; + + provider.setSelection(new TextSelection(pos.offset, pos.length)); + } + } finally { + TextEditorPlugin.getDefault().setMovedSinceLastEditRecall(false); + } + } + + @Override + public void selectionChanged(IAction action, ISelection selection) { + boolean enabled= TextEditorPlugin.getDefault().getLastEditPosition() != null; + setEnabled(enabled); + action.setEnabled(enabled); + + // This is no longer needed once the action is enabled. + if (!enabled) { + // adding the same action twice has no effect. + TextEditorPlugin.getDefault().addLastEditPositionDependentAction(action); + // this is always the same action for this instance + fAction = action; + } + } + + /** + * Returns the workbench window. + * + * @return the workbench window + */ + private IWorkbenchWindow getWindow() { + if (fWindow == null) + fWindow= PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + return fWindow; + } + + @Override + public void dispose() { + fWindow= null; + TextEditorPlugin.getDefault().removeLastEditPositionDependentAction(fAction); + } +} diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/IAbstractTextEditorHelpContextIds.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/IAbstractTextEditorHelpContextIds.java index 22e039b4273..d05fd2b8845 100644 --- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/IAbstractTextEditorHelpContextIds.java +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/IAbstractTextEditorHelpContextIds.java @@ -299,8 +299,17 @@ public interface IAbstractTextEditorHelpContextIds { String GOTO_LAST_EDIT_POSITION_ACTION= PREFIX + "goto_last_edit_position" + ACTION_POSTFIX; //$NON-NLS-1$ /** - * Help context id for the action. - * Value: <code>"org.eclipse.ui.move_lines_action_context"</code> + * Help context id for the action. Value: + * <code>"org.eclipse.ui.goto_next_edit_position_action_context"</code> + * + * @since 3.15 + */ + String GOTO_NEXT_EDIT_POSITION_ACTION = PREFIX + "goto_next_edit_position" + ACTION_POSTFIX; //$NON-NLS-1$ + + /** + * Help context id for the action. Value: + * <code>"org.eclipse.ui.move_lines_action_context"</code> + * * @since 3.0 */ String MOVE_LINES_ACTION= PREFIX + "move_lines" + ACTION_POSTFIX; //$NON-NLS-1$ diff --git a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/ITextEditorActionDefinitionIds.java b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/ITextEditorActionDefinitionIds.java index ca9a78d948f..2168fa25728 100644 --- a/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/ITextEditorActionDefinitionIds.java +++ b/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/ITextEditorActionDefinitionIds.java @@ -442,6 +442,14 @@ public interface ITextEditorActionDefinitionIds extends IWorkbenchActionDefiniti String GOTO_LAST_EDIT_POSITION= "org.eclipse.ui.edit.text.gotoLastEditPosition"; //$NON-NLS-1$ /** + * Action definition id of go to next edit position action. Value: + * <code>"org.eclipse.ui.edit.text.gotoNextEditPosition"</code> + * + * @since 3.15 + */ + String GOTO_NEXT_EDIT_POSITION= "org.eclipse.ui.edit.text.gotoNextEditPosition"; //$NON-NLS-1$ + + /** * Action definition id of go to next annotation action. * Value: <code>"org.eclipse.ui.edit.text.gotoNextAnnotation"</code> * @since 3.0 |