Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAri Kast2020-07-20 00:13:49 +0000
committerAri Kast2020-08-07 00:33:40 +0000
commitc7fa061d62bdd7853035da3a76a467e9de9b4b93 (patch)
tree1b393d11c4f169856bc56ee75684ce8bac0a34ea
parent746a67fdb4b15bee4c9e564a8807029400ac3eb2 (diff)
downloadeclipse.platform.text-c7fa061d62bdd7853035da3a76a467e9de9b4b93.tar.gz
eclipse.platform.text-c7fa061d62bdd7853035da3a76a467e9de9b4b93.tar.xz
eclipse.platform.text-c7fa061d62bdd7853035da3a76a467e9de9b4b93.zip
Bug 72773: store and navigate multiple edit history locations
This expands existing "last edit location" navigation to now store an ordered queue of multiple last edit locations. The size of the list is configurable (currently set at 15). Once the list is filled, each new location insertion evicts the oldest location to make room for the new. Thus only the last 15 edit locations are ever stored. To avoid wasting list space on multiple similar locations in close proximity to each other, similar edit locations (as determined by a parameterized proximity function) are merged to a single location. Ctrl+Q key mapping still navigates to the last edit location same as before. However, now continuing to hold ctrl and then pressing Q again begins the traversal thru history of prior edit locations. Once traversal stops, future Ctrl+Q actions are now temporarily anchored to this older historical location. M3+Ctrl+Q conversely moves the anchor forward thru edit history, so after traversing backward with Ctrl+Q, you can go forward again by repeatedly pressing Alt/Opt+Ctrl+Q. New edit locations are always inserted at the end of the queue, so that insertion order is always maintained. Insertion of a new edit location also resets the last location "anchor" back to the most recent edit, so that pressing Ctrl+Q once again brings you to the most recent edit rather than a historical one. Change-Id: I885e209b9d9300f8e6226e6cdf64616751ead944
-rw-r--r--org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos.pngbin0 -> 529 bytes
-rw-r--r--org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos@2x.pngbin0 -> 1081 bytes
-rw-r--r--org.eclipse.ui.editors/icons/full/etool16/next_edit_pos.pngbin0 -> 576 bytes
-rw-r--r--org.eclipse.ui.editors/icons/full/etool16/next_edit_pos@2x.pngbin0 -> 1190 bytes
-rw-r--r--org.eclipse.ui.editors/plugin.properties2
-rw-r--r--org.eclipse.ui.editors/plugin.xml13
-rw-r--r--org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/TextEditorPluginTest.java387
-rw-r--r--org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java3
-rw-r--r--org.eclipse.ui.workbench.texteditor/META-INF/MANIFEST.MF2
-rw-r--r--org.eclipse.ui.workbench.texteditor/plugin.properties2
-rw-r--r--org.eclipse.ui.workbench.texteditor/plugin.xml11
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditPosition.java49
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.java2
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/EditorMessages.properties2
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/HistoryTracker.java412
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/TextEditorPlugin.java110
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/AbstractTextEditor.java72
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoLastEditPositionAction.java91
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/GotoNextEditPositionAction.java162
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/IAbstractTextEditorHelpContextIds.java13
-rw-r--r--org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/texteditor/ITextEditorActionDefinitionIds.java8
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
new file mode 100644
index 00000000000..995f1badd96
--- /dev/null
+++ b/org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos.png
Binary files differ
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
new file mode 100644
index 00000000000..ee5c0432839
--- /dev/null
+++ b/org.eclipse.ui.editors/icons/full/dtool16/next_edit_pos@2x.png
Binary files differ
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
new file mode 100644
index 00000000000..5fa266b0760
--- /dev/null
+++ b/org.eclipse.ui.editors/icons/full/etool16/next_edit_pos.png
Binary files differ
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
new file mode 100644
index 00000000000..47c1f8a8159
--- /dev/null
+++ b/org.eclipse.ui.editors/icons/full/etool16/next_edit_pos@2x.png
Binary files differ
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

Back to the top