Bug 578709 - Multiple editor selection dialogs apppear on debugging

Serialized SourceLookup and SourceDisplay jobs.

All source lookups tasks are now put in the queue and handled by a
single job (not multiple jobs running in parallel). Once lookup is done,
the result is passed to the second job dedicated to show lookup results
in the UI. Both jobs ignore duplicated tasks.

As a result, if previously there was possible to run multiple source
lookups in parallel (that would compute same result) and all those
lookup results were shown one after each other in the UI (re-triggering
same UI actions again and again), now all source lookups are executed
one by one and there are no attempts made to show identical lookup
results again.

Change-Id: I1737910e4269e6795d6da1fd71bad1874cc59f7d
Reviewed-on: https://git.eclipse.org/r/c/platform/eclipse.platform.debug/+/190843
Reviewed-by: Simeon Andreev <simeon.danailov.andreev@gmail.com>
Reviewed-by: Andrey Loskutov <loskutov@gmx.de>
Tested-by: Andrey Loskutov <loskutov@gmx.de>
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/sourcelookup/SourceLookupFacilityTests.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/sourcelookup/SourceLookupFacilityTests.java
index fd96329..8535bd4 100644
--- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/sourcelookup/SourceLookupFacilityTests.java
+++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/sourcelookup/SourceLookupFacilityTests.java
@@ -21,8 +21,9 @@
 import static org.junit.Assert.assertTrue;
 
 import java.lang.reflect.Field;
-import java.util.HashMap;
+import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
+import java.util.Map;
 
 import org.eclipse.debug.core.model.IStackFrame;
 import org.eclipse.debug.internal.ui.sourcelookup.SourceLookupFacility;
@@ -317,7 +318,7 @@
 			// Get the original map
 			Field field = SourceLookupFacility.class.getDeclaredField("fLookupResults"); //$NON-NLS-1$
 			field.setAccessible(true);
-			HashMap<?, ?> map = (HashMap<?, ?>) field.get(null);
+			Map<?, ?> map = (Map<?, ?>) field.get(SourceLookupFacility.getDefault());
 			LinkedHashMap<String, ISourceLookupResult> cached = new LinkedHashMap<>();
 
 			// fill the LRU with one element overflow
@@ -343,7 +344,7 @@
 			String artifact = "" + 0; //$NON-NLS-1$
 			SourceLookupResult result = SourceLookupFacility.getDefault().lookup(artifact, fTestLocator, false);
 			assertNotNull("There should be a result", result); //$NON-NLS-1$
-			assertFalse(cached.containsValue(result));
+			assertFalse(new IdentityHashMap<>(cached).containsValue(result));
 
 			// Check: the LRU map size should not grow
 			assertEquals(MAX_LRU_SIZE, map.size());
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/sourcelookup/SourceLookupFacility.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/sourcelookup/SourceLookupFacility.java
index c356da3..74e359d 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/sourcelookup/SourceLookupFacility.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/sourcelookup/SourceLookupFacility.java
@@ -15,9 +15,13 @@
  *******************************************************************************/
 package org.eclipse.debug.internal.ui.sourcelookup;
 
-import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
 import java.util.Map;
+import java.util.Objects;
 
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
@@ -88,10 +92,9 @@
 	 *
 	 * @since 3.10
 	 */
-	static class LRU extends HashMap<Object, SourceLookupResult> {
+	static class LRU extends LinkedHashMap<Object, SourceLookupResult> {
 		private static final long serialVersionUID = 1L;
 
-		ArrayList<Object> fEntryStack = null;
 		int fSize;
 
 		/**
@@ -100,44 +103,14 @@
 		 * @param size The desired size
 		 */
 		LRU(int size) {
+			// true == use this map like LRU cache
+			super(size, 0.75f, true);
 			fSize = size;
-			fEntryStack = new ArrayList<>();
 		}
 
 		@Override
-		public SourceLookupResult put(Object key, SourceLookupResult value) {
-			shuffle(key);
-			return super.put(key, value);
-		}
-
-		@Override
-		public SourceLookupResult remove(Object key) {
-			SourceLookupResult oldResult = super.remove(key);
-			fEntryStack.remove(key);
-			return oldResult;
-		}
-
-		/**
-		 * Shuffles the entry stack and removes mapped results as needed
-		 *
-		 * @param key
-		 */
-		void shuffle(Object key) {
-			int index = fEntryStack.indexOf(key);
-			if (index < 0) {
-				if (fEntryStack.size() >= fSize) {
-					remove(fEntryStack.get(fEntryStack.size() - 1));
-				}
-			} else {
-				fEntryStack.remove(index);
-			}
-			fEntryStack.add(0, key);
-		}
-
-		@Override
-		public void clear() {
-			fEntryStack.clear();
-			super.clear();
+		protected boolean removeEldestEntry(Map.Entry<Object, SourceLookupResult> eldest) {
+			return size() > fSize;
 		}
 	}
 
@@ -158,7 +131,7 @@
 	 *
 	 * @since 3.10
 	 */
-	private static LRU fLookupResults = new LRU(10);
+	private final Map<Object, SourceLookupResult> fLookupResults = Collections.synchronizedMap(new LRU(10));
 
 	/**
 	 * Used to generate annotations for stack frames
@@ -170,6 +143,9 @@
 	 */
 	private boolean fReuseEditor = DebugUIPlugin.getDefault().getPreferenceStore().getBoolean(IDebugUIConstants.PREF_REUSE_EDITOR);
 
+	/** Singleton job to process source lookup requests */
+	private final SourceLookupJob sourceLookupJob;
+
 	/**
 	 * Constructs singleton source display adapter for stack frames.
 	 */
@@ -199,6 +175,7 @@
 	 */
 	private SourceLookupFacility() {
 		fEditorsByPage = new HashMap<>();
+		sourceLookupJob = new SourceLookupJob();
 		DebugUIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this);
 		DebugPlugin.getDefault().addDebugEventListener(this);
 	}
@@ -237,7 +214,7 @@
 		}
 	}
 
-	private class ArtifactWithLocator {
+	private static class ArtifactWithLocator {
 		public final Object artifact;
 		public final ISourceLocator locator;
 		public ArtifactWithLocator(Object artifact, ISourceLocator locator) {
@@ -247,12 +224,7 @@
 
 		@Override
 		public int hashCode() {
-			final int prime = 31;
-			int result = 1;
-			result = prime * result + getOuterType().hashCode();
-			result = prime * result + ((artifact == null) ? 0 : artifact.hashCode());
-			result = prime * result + ((locator == null) ? 0 : locator.hashCode());
-			return result;
+			return 31 + Objects.hash(artifact, locator);
 		}
 
 		@Override
@@ -260,36 +232,30 @@
 			if (this == obj) {
 				return true;
 			}
-			if (obj == null) {
-				return false;
-			}
-			if (getClass() != obj.getClass()) {
+			if (!(obj instanceof ArtifactWithLocator)) {
 				return false;
 			}
 			ArtifactWithLocator other = (ArtifactWithLocator) obj;
-			if (!getOuterType().equals(other.getOuterType())) {
-				return false;
-			}
-			if (artifact == null) {
-				if (other.artifact != null) {
-					return false;
-				}
-			} else if (!artifact.equals(other.artifact)) {
-				return false;
-			}
-			if (locator == null) {
-				if (other.locator != null) {
-					return false;
-				}
-			} else if (!locator.equals(other.locator)) {
-				return false;
-			}
-			return true;
+			return Objects.equals(artifact, other.artifact) && Objects.equals(locator, other.locator);
 		}
 
-		private SourceLookupFacility getOuterType() {
-			return SourceLookupFacility.this;
+		@Override
+		public String toString() {
+			StringBuilder builder = new StringBuilder();
+			builder.append("ArtifactWithLocator ["); //$NON-NLS-1$
+			if (artifact != null) {
+				builder.append("artifact="); //$NON-NLS-1$
+				builder.append(artifact);
+				builder.append(", "); //$NON-NLS-1$
+			}
+			if (locator != null) {
+				builder.append("locator="); //$NON-NLS-1$
+				builder.append(locator);
+			}
+			builder.append("]"); //$NON-NLS-1$
+			return builder.toString();
 		}
+
 	}
 
 	/**
@@ -305,74 +271,70 @@
 	 */
 	public SourceLookupResult lookup(Object artifact, ISourceLocator locator, boolean force) {
 		SourceLookupResult result = null;
-		synchronized (fLookupResults) {
-			ArtifactWithLocator key = new ArtifactWithLocator(artifact, locator);
-			if (!force) {
-				result = fLookupResults.get(key);
-				if (result != null) {
-					return result;
-				}
-			}
-			result = new SourceLookupResult(artifact, null, null, null);
-			IDebugElement debugElement = null;
-			if (artifact instanceof IDebugElement) {
-				debugElement = (IDebugElement) artifact;
-			}
-			ISourceLocator localLocator = locator;
-			if (localLocator == null) {
-				ILaunch launch = null;
-				if (debugElement != null) {
-					launch = debugElement.getLaunch();
-				}
-				if (launch != null) {
-					localLocator = launch.getSourceLocator();
-				}
-			}
-			if (localLocator != null) {
-				String editorId = null;
-				IEditorInput editorInput = null;
-				Object sourceElement = null;
-				if (localLocator instanceof ISourceLookupDirector) {
-					ISourceLookupDirector director = (ISourceLookupDirector) localLocator;
-					sourceElement = director.getSourceElement(artifact);
-				} else {
-					if (artifact instanceof IStackFrame) {
-						sourceElement = localLocator.getSourceElement((IStackFrame) artifact);
-					}
-				}
-				if (sourceElement == null) {
-					if (localLocator instanceof AbstractSourceLookupDirector) {
-						editorInput = new CommonSourceNotFoundEditorInput(artifact);
-						editorId = IDebugUIConstants.ID_COMMON_SOURCE_NOT_FOUND_EDITOR;
-					} else {
-						if (artifact instanceof IStackFrame) {
-							IStackFrame frame = (IStackFrame) artifact;
-							editorInput = new SourceNotFoundEditorInput(frame);
-							editorId = IInternalDebugUIConstants.ID_SOURCE_NOT_FOUND_EDITOR;
-						}
-					}
-				} else {
-					ISourcePresentation presentation = null;
-					if (localLocator instanceof ISourcePresentation) {
-						presentation = (ISourcePresentation) localLocator;
-					} else {
-						if (debugElement != null) {
-							presentation = getPresentation(debugElement.getModelIdentifier());
-						}
-					}
-					if (presentation != null) {
-						editorInput = presentation.getEditorInput(sourceElement);
-					}
-					if (editorInput != null && presentation != null) {
-						editorId = presentation.getEditorId(editorInput, sourceElement);
-					}
-				}
-				result.setEditorInput(editorInput);
-				result.setEditorId(editorId);
-				result.setSourceElement(sourceElement);
-				fLookupResults.put(key, result);
+		ArtifactWithLocator key = new ArtifactWithLocator(artifact, locator);
+		if (!force) {
+			result = fLookupResults.get(key);
+			if (result != null) {
+				return result;
 			}
 		}
+		IDebugElement debugElement = null;
+		if (artifact instanceof IDebugElement) {
+			debugElement = (IDebugElement) artifact;
+		}
+		ISourceLocator localLocator = locator;
+		if (localLocator == null) {
+			ILaunch launch = null;
+			if (debugElement != null) {
+				launch = debugElement.getLaunch();
+			}
+			if (launch != null) {
+				localLocator = launch.getSourceLocator();
+			}
+		}
+		if (localLocator == null) {
+			return new SourceLookupResult(artifact, null, null, null);
+		}
+		String editorId = null;
+		IEditorInput editorInput = null;
+		Object sourceElement = null;
+		if (localLocator instanceof ISourceLookupDirector) {
+			ISourceLookupDirector director = (ISourceLookupDirector) localLocator;
+			sourceElement = director.getSourceElement(artifact);
+		} else {
+			if (artifact instanceof IStackFrame) {
+				sourceElement = localLocator.getSourceElement((IStackFrame) artifact);
+			}
+		}
+		if (sourceElement == null) {
+			if (localLocator instanceof AbstractSourceLookupDirector) {
+				editorInput = new CommonSourceNotFoundEditorInput(artifact);
+				editorId = IDebugUIConstants.ID_COMMON_SOURCE_NOT_FOUND_EDITOR;
+			} else {
+				if (artifact instanceof IStackFrame) {
+					IStackFrame frame = (IStackFrame) artifact;
+					editorInput = new SourceNotFoundEditorInput(frame);
+					editorId = IInternalDebugUIConstants.ID_SOURCE_NOT_FOUND_EDITOR;
+				}
+			}
+		} else {
+			ISourcePresentation presentation = null;
+			if (localLocator instanceof ISourcePresentation) {
+				presentation = (ISourcePresentation) localLocator;
+			} else {
+				if (debugElement != null) {
+					presentation = getPresentation(debugElement.getModelIdentifier());
+				}
+			}
+			if (presentation != null) {
+				editorInput = presentation.getEditorInput(sourceElement);
+			}
+			if (editorInput != null && presentation != null) {
+				editorId = presentation.getEditorId(editorInput, sourceElement);
+			}
+		}
+		result = new SourceLookupResult(artifact, sourceElement, editorId, editorInput);
+		fLookupResults.put(key, result);
 		return result;
 	}
 
@@ -692,110 +654,283 @@
 		fEditorsByPage.clear();
 		fPresentation.dispose();
 		fLookupResults.clear();
+		sourceLookupJob.cancel();
 	}
 
 	/**
-	 * A job to perform source lookup on the currently selected stack frame.
+	 * A singleton job to perform source lookups via given {@link SourceLookupTask}
+	 * objects. The tasks are put in the queue to process in the background,
+	 * duplicated tasks are ignored. Job re-schedules itself if new task is added to
+	 * the queue.
 	 */
-	class SourceLookupJob extends Job {
+	final class SourceLookupJob extends Job {
 
-		private IStackFrame fTarget;
-		private ISourceLocator fLocator;
-		private IWorkbenchPage fPage;
-		private boolean fForce = false;
+		private final LinkedHashSet<SourceLookupTask> queue;
+		private final SourceDisplayJob sourceDisplayJob;
 
-		/**
-		 * Constructs a new source lookup job.
-		 */
-		public SourceLookupJob(IStackFrame frame, ISourceLocator locator, IWorkbenchPage page, boolean force) {
+		public SourceLookupJob() {
 			super("Debug Source Lookup"); //$NON-NLS-1$
-			setPriority(Job.INTERACTIVE);
+			this.sourceDisplayJob = new SourceDisplayJob();
+			this.queue = new LinkedHashSet<>();
 			setSystem(true);
-			fTarget = frame;
-			fLocator = locator;
-			fPage = page;
-			fForce = force;
+			setPriority(Job.INTERACTIVE);
 			// Note: Be careful when trying to use scheduling rules with this
 			// job, in order to avoid blocking nested jobs (bug 339542).
 		}
 
 		@Override
+		public boolean belongsTo(Object family) {
+			return family instanceof SourceLookupFacility;
+		}
+
+		@Override
 		protected IStatus run(IProgressMonitor monitor) {
-			if (!monitor.isCanceled()) {
-				if (!fTarget.isTerminated()) {
-					ISourceLookupResult result = lookup(fTarget, fLocator, fForce);
-					if (!monitor.isCanceled() && !fTarget.isTerminated() && fPage != null) {
-						new SourceDisplayJob(result, fPage).schedule();
-					}
+			SourceLookupTask next;
+			while ((next = poll()) != null && !monitor.isCanceled()) {
+				SourceDisplayRequest uiTask = next.run(monitor);
+				if (uiTask != null) {
+					sourceDisplayJob.schedule(uiTask);
+				}
+			}
+
+			synchronized (queue) {
+				if (monitor.isCanceled()) {
+					queue.clear();
+					return Status.CANCEL_STATUS;
+				} else if (!queue.isEmpty()) {
+					schedule(100);
 				}
 			}
 			return Status.OK_STATUS;
 		}
 
-		@Override
-		public boolean belongsTo(Object family) {
-			// source lookup jobs are a family per workbench page
-			if (family instanceof SourceLookupJob) {
-				SourceLookupJob slj = (SourceLookupJob) family;
-				return slj.fPage.equals(fPage);
+		private SourceLookupTask poll() {
+			SourceLookupTask next = null;
+			synchronized (queue) {
+				if (!queue.isEmpty()) {
+					Iterator<SourceLookupTask> iterator = queue.iterator();
+					next = iterator.next();
+					iterator.remove();
+				}
 			}
-			return false;
+			return next;
+		}
+
+		void schedule(SourceLookupTask task) {
+			synchronized (queue) {
+				boolean added = queue.add(task);
+				if (added) {
+					schedule(100);
+				}
+			}
+		}
+	}
+
+	/**
+	 * A task to perform source lookup on the currently selected stack frame.
+	 */
+	class SourceLookupTask {
+
+		final IStackFrame fTarget;
+		final ISourceLocator fLocator;
+		final IWorkbenchPage fPage;
+		final boolean fForce;
+
+		/**
+		 * Constructs a new source lookup task.
+		 */
+		public SourceLookupTask(IStackFrame frame, ISourceLocator locator, IWorkbenchPage page, boolean force) {
+			fTarget = frame;
+			fLocator = locator;
+			fPage = page;
+			fForce = force;
+		}
+
+		protected SourceDisplayRequest run(IProgressMonitor monitor) {
+			if (!monitor.isCanceled()) {
+				if (!fTarget.isTerminated()) {
+					SourceLookupResult result = lookup(fTarget, fLocator, fForce);
+					if (!monitor.isCanceled() && !fTarget.isTerminated() && fPage != null && result != null) {
+						return new SourceDisplayRequest(result, fPage);
+					}
+				}
+			}
+			return null;
+		}
+
+		@Override
+		public int hashCode() {
+			return 31 + Objects.hash(fForce, fLocator, fPage, fTarget);
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (this == obj) {
+				return true;
+			}
+			if (!(obj instanceof SourceLookupTask)) {
+				return false;
+			}
+			SourceLookupTask other = (SourceLookupTask) obj;
+			return fForce == other.fForce && Objects.equals(fPage, other.fPage)
+					&& Objects.equals(fLocator, other.fLocator) && Objects.equals(fTarget, other.fTarget);
+		}
+
+		@Override
+		public String toString() {
+			StringBuilder builder = new StringBuilder();
+			builder.append("SourceLookupTask ["); //$NON-NLS-1$
+			if (fTarget != null) {
+				builder.append("target="); //$NON-NLS-1$
+				builder.append(fTarget);
+				builder.append(", "); //$NON-NLS-1$
+			}
+			builder.append("force="); //$NON-NLS-1$
+			builder.append(fForce);
+			builder.append(", "); //$NON-NLS-1$
+			if (fLocator != null) {
+				builder.append("locator="); //$NON-NLS-1$
+				builder.append(fLocator);
+				builder.append(", "); //$NON-NLS-1$
+			}
+			if (fPage != null) {
+				builder.append("page="); //$NON-NLS-1$
+				builder.append(fPage);
+			}
+			builder.append("]"); //$NON-NLS-1$
+			return builder.toString();
 		}
 
 	}
 
-	class SourceDisplayJob extends UIJob {
+	/**
+	 * A request to show the result of the source lookup in the UI
+	 */
+	static class SourceDisplayRequest {
 
-		private ISourceLookupResult fResult;
-		private IWorkbenchPage fPage;
+		final SourceLookupResult fResult;
+		final IWorkbenchPage fPage;
 
-		public SourceDisplayJob(ISourceLookupResult result, IWorkbenchPage page) {
-			super("Debug Source Display"); //$NON-NLS-1$
-			setSystem(true);
-			setPriority(Job.INTERACTIVE);
+		public SourceDisplayRequest(SourceLookupResult result, IWorkbenchPage page) {
 			fResult = result;
 			fPage = page;
 		}
 
 		@Override
+		public int hashCode() {
+			return Objects.hash(fPage, fResult);
+		}
+
+		@Override
+		public boolean equals(Object obj) {
+			if (this == obj) {
+				return true;
+			}
+			if (!(obj instanceof SourceDisplayRequest)) {
+				return false;
+			}
+			SourceDisplayRequest other = (SourceDisplayRequest) obj;
+			return Objects.equals(fPage, other.fPage) && Objects.equals(fResult, other.fResult);
+		}
+
+		@Override
+		public String toString() {
+			StringBuilder builder = new StringBuilder();
+			builder.append("SourceDisplayRequest ["); //$NON-NLS-1$
+			if (fResult != null) {
+				builder.append("result="); //$NON-NLS-1$
+				builder.append(fResult);
+				builder.append(", "); //$NON-NLS-1$
+			}
+			if (fPage != null) {
+				builder.append("page="); //$NON-NLS-1$
+				builder.append(fPage);
+			}
+			builder.append("]"); //$NON-NLS-1$
+			return builder.toString();
+		}
+
+	}
+
+	/**
+	 * A singleton job to show the result of the source lookup in the UI for given
+	 * {@link SourceDisplayRequest} objects. The requests are put in the queue to
+	 * process in the background, duplicated requests are ignored. Job re-schedules
+	 * itself if new request is added to the queue.
+	 */
+	class SourceDisplayJob extends UIJob {
+
+		private final LinkedHashSet<SourceDisplayRequest> queue;
+
+		public SourceDisplayJob() {
+			super("Debug Source Display"); //$NON-NLS-1$
+			setSystem(true);
+			setPriority(Job.INTERACTIVE);
+			this.queue = new LinkedHashSet<>();
+		}
+
+
+		@Override
 		public IStatus runInUIThread(IProgressMonitor monitor) {
-			if (!monitor.isCanceled() && fResult != null) {
-				display(fResult, fPage);
-				// termination may have occurred while displaying source
-				if (monitor.isCanceled()) {
-					Object artifact = fResult.getArtifact();
+			SourceDisplayRequest next;
+			// Do not break on cancelled monitor, to allow remove debugger
+			// annotations from already opened editors
+			while ((next = poll()) != null) {
+				IWorkbenchPage page = next.fPage;
+				if (page.getWorkbenchWindow() == null) {
+					// don't try to update if page is closed
+					continue;
+				}
+				ISourceLookupResult result = next.fResult;
+				if (!monitor.isCanceled()) {
+					display(result, page);
+				} else {
+					// termination may have occurred while displaying source
+					Object artifact = result.getArtifact();
 					if (artifact instanceof IStackFrame) {
 						clearSourceSelection(((IStackFrame) artifact).getThread());
 					}
 				}
 			}
-
 			return Status.OK_STATUS;
 		}
 
+		private SourceDisplayRequest poll() {
+			SourceDisplayRequest next = null;
+			synchronized (queue) {
+				if (!queue.isEmpty()) {
+					Iterator<SourceDisplayRequest> iterator = queue.iterator();
+					next = iterator.next();
+					iterator.remove();
+				}
+			}
+			return next;
+		}
+
+		void schedule(SourceDisplayRequest task) {
+			synchronized (queue) {
+				boolean added = queue.add(task);
+				if (added) {
+					schedule(100);
+				}
+			}
+		}
+
 		@Override
 		public boolean belongsTo(Object family) {
-			// source display jobs are a family per workbench page
-			if (family instanceof SourceDisplayJob) {
-				SourceDisplayJob sdj = (SourceDisplayJob) family;
-				return sdj.fPage.equals(fPage);
-			}
-			return false;
+			return family instanceof SourceLookupFacility;
 		}
 
 	}
 
 	/*
-	 * @see
-	 * org.eclipse.debug.ui.contexts.ISourceDisplayAdapter#displaySource(java
-	 * .lang.Object, org.eclipse.ui.IWorkbenchPage, boolean)
+	 * See org.eclipse.debug.ui.sourcelookup.ISourceDisplay
 	 */
-	public synchronized void displaySource(Object context, IWorkbenchPage page, boolean force) {
+	public void displaySource(Object context, IWorkbenchPage page, boolean force) {
 		IStackFrame frame = (IStackFrame) context;
-		SourceLookupJob slj = new SourceLookupJob(frame, frame.getLaunch().getSourceLocator(), page, force);
-		// cancel any existing source lookup jobs for this page
-		Job.getJobManager().cancel(slj);
-		slj.schedule();
+		SourceLookupTask slj = new SourceLookupTask(frame, frame.getLaunch().getSourceLocator(), page, force);
+		// will drop any existing equal source lookup jobs
+		sourceLookupJob.schedule(slj);
 	}
 
 	/**
diff --git a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/sourcelookup/SourceLookupResult.java b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/sourcelookup/SourceLookupResult.java
index c524b8c..55ab389 100644
--- a/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/sourcelookup/SourceLookupResult.java
+++ b/org.eclipse.debug.ui/ui/org/eclipse/debug/internal/ui/sourcelookup/SourceLookupResult.java
@@ -13,6 +13,8 @@
  *******************************************************************************/
 package org.eclipse.debug.internal.ui.sourcelookup;
 
+import java.util.Objects;
+
 import org.eclipse.debug.ui.sourcelookup.ISourceLookupResult;
 import org.eclipse.ui.IEditorInput;
 
@@ -27,22 +29,22 @@
 	/**
 	 * Element that source was resolved for.
 	 */
-	private Object fArtifact;
+	private final Object fArtifact;
 	/**
 	 * Corresponding source element, or <code>null</code>
 	 * if unknown.
 	 */
-	private Object fSourceElement;
+	private final Object fSourceElement;
 	/**
 	 * Associated editor id, used to display the source element,
 	 * or <code>null</code> if unknown.
 	 */
-	private String fEditorId;
+	private final String fEditorId;
 	/**
 	 * Associated editor input, used to display the source element,
 	 * or <code>null</code> if unknown.
 	 */
-	private IEditorInput fEditorInput;
+	private final IEditorInput fEditorInput;
 
 	/**
 	 * Creates a source lookup result on the given artifact, source element,
@@ -50,9 +52,9 @@
 	 */
 	public SourceLookupResult(Object artifact, Object sourceElement, String editorId, IEditorInput editorInput) {
 		fArtifact = artifact;
-		setSourceElement(sourceElement);
-		setEditorId(editorId);
-		setEditorInput(editorInput);
+		fSourceElement = sourceElement;
+		fEditorId = editorId;
+		fEditorInput = editorInput;
 	}
 
 	@Override
@@ -65,60 +67,63 @@
 		return fSourceElement;
 	}
 
-	/**
-	 * Sets the source element resolved for the artifact that source
-	 * lookup was performed for, or <code>null</code> if a source element
-	 * was not resolved.
-	 *
-	 * @param element resolved source element or <code>null</code> if unknown
-	 */
-	protected void setSourceElement(Object element) {
-		fSourceElement = element;
-	}
-
 	@Override
 	public String getEditorId() {
 		return fEditorId;
 	}
 
-	/**
-	 * Sets the identifier of the editor used to display this source
-	 * lookup result's source element, or <code>null</code> if unknown.
-	 *
-	 * @param id the identifier of the editor used to display this source
-	 * lookup result's source element, or <code>null</code> if unknown
-	 */
-	protected void setEditorId(String id) {
-		fEditorId = id;
-	}
-
 	@Override
 	public IEditorInput getEditorInput() {
 		return fEditorInput;
 	}
 
-	/**
-	 * Sets the editor input used to display this source lookup
-	 * result's source element, or <code>null</code> if unknown.
-	 *
-	 * @param input the editor input used to display this source lookup
-	 * result's source element, or <code>null</code> if unknown
-	 */
-	protected void setEditorInput(IEditorInput input) {
-		fEditorInput = input;
+	@Override
+	public int hashCode() {
+		return Objects.hash(fArtifact, fEditorId, fEditorInput, fSourceElement);
 	}
 
-	/**
-	 * Updates the artifact to refer to the given artifact
-	 * if equal. For example, when a source lookup result is reused
-	 * for the same stack frame, we still need to update in case
-	 * the stack frame is not identical.
-	 *
-	 * @param artifact new artifact state
-	 */
-	public void updateArtifact(Object artifact) {
-		if (fArtifact.equals(artifact)) {
-			fArtifact = artifact;
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj) {
+			return true;
 		}
+
+		if (!(obj instanceof SourceLookupResult)) {
+			return false;
+		}
+
+		SourceLookupResult other = (SourceLookupResult) obj;
+		return Objects.equals(fEditorId, other.fEditorId)
+				&& Objects.equals(fArtifact, other.fArtifact)
+				&& Objects.equals(fEditorInput, other.fEditorInput)
+				&& Objects.equals(fSourceElement, other.fSourceElement);
 	}
+
+	@Override
+	public String toString() {
+		StringBuilder builder = new StringBuilder();
+		builder.append("SourceLookupResult ["); //$NON-NLS-1$
+		if (fEditorId != null) {
+			builder.append("editorId="); //$NON-NLS-1$
+			builder.append(fEditorId);
+			builder.append(", "); //$NON-NLS-1$
+		}
+		if (fEditorInput != null) {
+			builder.append("editorInput="); //$NON-NLS-1$
+			builder.append(fEditorInput);
+			builder.append(", "); //$NON-NLS-1$
+		}
+		if (fArtifact != null) {
+			builder.append("artifact="); //$NON-NLS-1$
+			builder.append(fArtifact);
+			builder.append(", "); //$NON-NLS-1$
+		}
+		if (fSourceElement != null) {
+			builder.append("sourceElement="); //$NON-NLS-1$
+			builder.append(fSourceElement);
+		}
+		builder.append("]"); //$NON-NLS-1$
+		return builder.toString();
+	}
+
 }