Bug 541714: [SourceEditor] Revise InfoHover / editor hovers

  - Let run InfoHover.getHoverInfo always in non-UI thread
    supporting also longer running tasks (progress monitor)
  - Add use of nullable annotations

Change-Id: Ic6016bc34ca1930c5a10016599153374bd06189c
diff --git a/ltk/org.eclipse.statet.ltk.ui/META-INF/MANIFEST.MF b/ltk/org.eclipse.statet.ltk.ui/META-INF/MANIFEST.MF
index 3a1bf98..b52636c 100644
--- a/ltk/org.eclipse.statet.ltk.ui/META-INF/MANIFEST.MF
+++ b/ltk/org.eclipse.statet.ltk.ui/META-INF/MANIFEST.MF
@@ -35,6 +35,7 @@
  org.eclipse.statet.ecommons.resources.core;version="4.0.0",
  org.eclipse.statet.jcommons.collections;version="4.0.0",
  org.eclipse.statet.jcommons.lang;version="4.0.0",
+ org.eclipse.statet.jcommons.status;version="4.0.0",
  org.eclipse.statet.jcommons.text.core;version="4.0.0"
 Export-Package: org.eclipse.statet.ecommons.templates,
  org.eclipse.statet.ecommons.text.ui,
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/EditorInformationProvider.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/EditorInformationProvider.java
index ccc460f..0e6ac9d 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/EditorInformationProvider.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/EditorInformationProvider.java
@@ -14,10 +14,14 @@
 
 package org.eclipse.statet.ltk.ui.sourceediting;
 
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.atomic.AtomicReference;
+
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
 import org.eclipse.core.runtime.Status;
-import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
 import org.eclipse.jface.text.IInformationControlCreator;
 import org.eclipse.jface.text.IRegion;
 import org.eclipse.jface.text.ITextViewer;
@@ -25,27 +29,39 @@
 import org.eclipse.jface.text.information.IInformationProvider;
 import org.eclipse.jface.text.information.IInformationProviderExtension;
 import org.eclipse.jface.text.information.IInformationProviderExtension2;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.progress.IProgressService;
 import org.eclipse.ui.statushandlers.StatusManager;
 
+import org.eclipse.statet.jcommons.collections.ImList;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+import org.eclipse.statet.jcommons.status.InterruptNullProgressMonitor;
+import org.eclipse.statet.jcommons.status.ProgressMonitor;
+import org.eclipse.statet.jcommons.status.StatusException;
+
+import org.eclipse.statet.ecommons.runtime.core.util.StatusUtils;
 import org.eclipse.statet.ecommons.text.core.util.TextUtils;
 
 import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin;
 import org.eclipse.statet.ltk.ui.sourceediting.assist.AssistInvocationContext;
-import org.eclipse.statet.ltk.ui.sourceediting.assist.IInfoHover;
+import org.eclipse.statet.ltk.ui.sourceediting.assist.InfoHover;
 
 
+@NonNullByDefault
 public abstract class EditorInformationProvider
 		implements IInformationProvider, IInformationProviderExtension, IInformationProviderExtension2 {
 	
 	
 	private final ISourceEditor editor;
 	
-	private final IInfoHover[] hovers;
+	private final ImList<? extends InfoHover> hovers;
 	
-	private IInfoHover bestHover;
+	private volatile @Nullable InfoHover bestHover;
 	
 	
-	public EditorInformationProvider(final ISourceEditor editor, final IInfoHover[] hovers) {
+	public EditorInformationProvider(final ISourceEditor editor,
+			final ImList<? extends InfoHover> hovers) {
 		this.editor= editor;
 		this.hovers= hovers;
 	}
@@ -56,14 +72,13 @@
 	}
 	
 	@Override
-	public String getInformation(final ITextViewer textViewer, final IRegion region) {
+	public @Nullable String getInformation(final ITextViewer textViewer, final IRegion region) {
 		return null;
 	}
 	
 	@Override
-	public Object getInformation2(final ITextViewer textViewer, final IRegion region) {
+	public @Nullable Object getInformation2(final ITextViewer textViewer, final IRegion region) {
 		this.bestHover= null;
-		final SubMonitor m= SubMonitor.convert(null);
 		try {
 			final String contentType= (region instanceof TypedRegion) ?
 					((TypedRegion) region).getType() :
@@ -71,14 +86,42 @@
 							this.editor.getDocumentContentInfo(), region.getOffset(),
 							region.getLength() == 0 );
 			
-			final AssistInvocationContext context= createContext(region, contentType, m);
+			final AssistInvocationContext context= createContext(region, contentType,
+					new NullProgressMonitor() );
 			if (context != null) {
-				for (int i= 0; i < this.hovers.length; i++) {
-					final Object info= this.hovers[i].getHoverInfo(context);
-					if (info != null) {
-						this.bestHover= this.hovers[i];
-						return info;
+				try {
+					final AtomicReference<Object> info= new AtomicReference<>();
+					if (Display.getCurrent() != null) {
+						try {
+							final IProgressService progressService= this.editor.getServiceLocator().getService(IProgressService.class);
+							progressService.run(true, true, new IRunnableWithProgress() {
+								@Override
+								public void run(final IProgressMonitor monitor)
+										throws InvocationTargetException, InterruptedException {
+									final ProgressMonitor m= StatusUtils.convert(monitor);
+									try {
+										getInfo0(context, info, m);
+									}
+									catch (final StatusException e) {
+										throw new InvocationTargetException(e);
+									}
+								}
+							});
+						}
+						catch (final InvocationTargetException e) {
+							throw (StatusException) e.getCause();
+						}
 					}
+					else {
+						getInfo0(context, info, new InterruptNullProgressMonitor());
+					}
+					return info.get();
+				}
+				catch (final InterruptedException e) {
+					return null;
+				}
+				catch (final StatusException e) {
+					return null;
 				}
 			}
 		}
@@ -89,15 +132,42 @@
 		return null;
 	}
 	
+	protected abstract @Nullable AssistInvocationContext createContext(IRegion region, String contentType,
+			IProgressMonitor monitor );
+	
+	private void getInfo0(final AssistInvocationContext context,
+			final AtomicReference<Object> infoValue,
+			final ProgressMonitor m) throws StatusException {
+		for (int i= 0; i < this.hovers.size(); i++) {
+			m.setWorkRemaining(this.hovers.size() - i);
+			if (m.isCanceled()) {
+				return;
+			}
+			try {
+				final InfoHover hover= this.hovers.get(i);
+				final Object info= hover.getHoverInfo(context, m.newSubMonitor(1));
+				if (info != null) {
+					this.bestHover= hover;
+					infoValue.set(info);
+					return;
+				}
+			}
+			catch (final StatusException e) {
+				if (e.getStatus().getSeverity() == Status.CANCEL) {
+					return;
+				}
+			}
+		}
+	}
+	
+	
 	@Override
-	public IInformationControlCreator getInformationPresenterControlCreator() {
-		if (this.bestHover != null) {
-			return this.bestHover.getHoverControlCreator();
+	public @Nullable IInformationControlCreator getInformationPresenterControlCreator() {
+		final InfoHover hover= this.bestHover;
+		if (hover != null) {
+			return hover.getHoverControlCreator();
 		}
 		return null;
 	}
 	
-	protected abstract AssistInvocationContext createContext(IRegion region, String contentType,
-			IProgressMonitor monitor );
-	
 }
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/EditorTextInfoHoverProxy.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/EditorTextInfoHoverProxy.java
index 69c9813..fbf5a91 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/EditorTextInfoHoverProxy.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/EditorTextInfoHoverProxy.java
@@ -14,6 +14,10 @@
 
 package org.eclipse.statet.ltk.ui.sourceediting;
 
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
+import java.util.concurrent.atomic.AtomicReference;
+
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.NullProgressMonitor;
@@ -28,28 +32,35 @@
 import org.eclipse.osgi.util.NLS;
 import org.eclipse.ui.statushandlers.StatusManager;
 
+import org.eclipse.statet.jcommons.collections.ImCollections;
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+import org.eclipse.statet.jcommons.status.InterruptNullProgressMonitor;
+import org.eclipse.statet.jcommons.status.ProgressMonitor;
+import org.eclipse.statet.jcommons.status.StatusException;
+
 import org.eclipse.statet.ecommons.text.core.util.TextUtils;
 
 import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin;
 import org.eclipse.statet.ltk.ui.sourceediting.assist.AssistInvocationContext;
 import org.eclipse.statet.ltk.ui.sourceediting.assist.CombinedHover;
-import org.eclipse.statet.ltk.ui.sourceediting.assist.IInfoHover;
+import org.eclipse.statet.ltk.ui.sourceediting.assist.InfoHover;
 import org.eclipse.statet.ltk.ui.sourceediting.assist.InfoHoverDescriptor;
 import org.eclipse.statet.ltk.ui.sourceediting.assist.InfoHoverRegistry.EffectiveHovers;
 
 
 /**
- * Wraps an LTK {@link IInfoHover} to an editor text hover.
+ * Wraps an LTK {@link InfoHover} to an editor text hover.
  */
+@NonNullByDefault
 public abstract class EditorTextInfoHoverProxy implements ITextHover, ITextHoverExtension, ITextHoverExtension2 {
 	
 	
 	private final InfoHoverDescriptor descriptor;
-	
-	private IInfoHover hover;
-	
 	private final SourceEditorViewerConfiguration sourceEditorConfig;
 	
+	private volatile @Nullable InfoHover hover;
+	
 	
 	public EditorTextInfoHoverProxy(final InfoHoverDescriptor descriptor,
 			final SourceEditorViewerConfiguration config) {
@@ -62,34 +73,44 @@
 		return this.sourceEditorConfig.getSourceEditor();
 	}
 	
+	@SuppressWarnings("null")
 	protected boolean ensureHover() {
-		if (this.hover == null) {
-			this.hover= this.descriptor.createHover();
-			if (this.hover instanceof CombinedHover) {
-				final EffectiveHovers effectiveHovers= this.sourceEditorConfig.getConfiguredInfoHovers();
-				if (effectiveHovers != null) {
-					((CombinedHover) this.hover).setHovers(effectiveHovers.getDescriptorsForCombined());
-				}
-				else {
-					this.hover= null;
+		InfoHover hover= this.hover;
+		if (hover == null) {
+			synchronized (this) {
+				hover= this.hover;
+				if (hover == null) {
+					hover= createHover();
+					this.hover= hover;
 				}
 			}
 		}
-		return (this.hover != null);
+		return (hover != null);
+	}
+	
+	private @Nullable InfoHover createHover() {
+		final InfoHover hover= this.descriptor.createHover();
+		if (hover instanceof CombinedHover) {
+			final EffectiveHovers effectiveHovers= this.sourceEditorConfig.getConfiguredInfoHovers();
+			((CombinedHover) hover).setHovers((effectiveHovers != null) ?
+					effectiveHovers.getDescriptorsForCombined() :
+					ImCollections.emptyList() );
+		}
+		return hover;
 	}
 	
 	@Override
-	public IRegion getHoverRegion(final ITextViewer textViewer, final int offset) {
+	public @Nullable IRegion getHoverRegion(final ITextViewer textViewer, final int offset) {
 		return null;
 	}
 	
 	@Override
-	public String getHoverInfo(final ITextViewer textViewer, final IRegion hoverRegion) {
+	public @Nullable String getHoverInfo(final ITextViewer textViewer, final IRegion hoverRegion) {
 		return null;
 	}
 	
 	@Override
-	public Object getHoverInfo2(final ITextViewer textViewer, final IRegion hoverRegion) {
+	public @Nullable Object getHoverInfo2(final ITextViewer textViewer, final IRegion hoverRegion) {
 		final ISourceEditor editor= getEditor();
 		if (editor != null && ensureHover()) {
 			try {
@@ -102,7 +123,14 @@
 				final AssistInvocationContext context= createContext(hoverRegion, contentType,
 						new NullProgressMonitor() );
 				if (context != null) {
-					return this.hover.getHoverInfo(context);
+					try {
+						final AtomicReference<Object> info= new AtomicReference<>();
+						getInfo0(context, info, new InterruptNullProgressMonitor());
+						return info.get();
+					}
+					catch (final StatusException e) {
+						return null;
+					}
 				}
 			}
 			catch (final Exception e) {
@@ -114,13 +142,36 @@
 		return null;
 	}
 	
-	protected abstract AssistInvocationContext createContext(IRegion region, String contentType,
+	protected abstract @Nullable AssistInvocationContext createContext(IRegion region, String contentType,
 			IProgressMonitor monitor );
 	
+	private void getInfo0(final AssistInvocationContext context,
+			final AtomicReference<Object> infoValue,
+			final ProgressMonitor m) throws StatusException {
+		try {
+			if (m.isCanceled()) {
+				return;
+			}
+			
+			final InfoHover hover= nonNullAssert(this.hover);
+			final Object info= hover.getHoverInfo(context, m);
+			if (info != null) {
+				infoValue.set(info);
+			}
+		}
+		catch (final StatusException e) {
+			if (e.getStatus().getSeverity() == Status.CANCEL) {
+				return;
+			}
+		}
+	}
+	
+	
 	@Override
-	public IInformationControlCreator getHoverControlCreator() {
-		if (ensureHover()) {
-			return this.hover.getHoverControlCreator();
+	public @Nullable IInformationControlCreator getHoverControlCreator() {
+		final InfoHover hover= this.hover;
+		if (hover != null) {
+			return hover.getHoverControlCreator();
 		}
 		return null;
 	}
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CombinedHover.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CombinedHover.java
index 2b2fa73..6fe41be 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CombinedHover.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/CombinedHover.java
@@ -19,46 +19,69 @@
 
 import org.eclipse.jface.text.IInformationControlCreator;
 
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+import org.eclipse.statet.jcommons.status.ProgressMonitor;
+import org.eclipse.statet.jcommons.status.StatusException;
+import org.eclipse.statet.jcommons.status.Statuses;
+
 
 /**
  * Special information hover showing the first match of the specified hover types.
  */
-public class CombinedHover implements IInfoHover {
+@NonNullByDefault
+public class CombinedHover implements InfoHover {
 	
 	
 	private List<InfoHoverDescriptor> descriptors;
-	private List<IInfoHover> instantiatedHovers;
+	private List<InfoHover> instantiatedHovers;
 	
-	private IInfoHover bestHover;
+	private volatile @Nullable InfoHover bestHover;
 	
 	
 	public CombinedHover() {
 	}
 	
 	
-	public void setHovers(final List<InfoHoverDescriptor> descriptors) {
+	public synchronized void setHovers(final List<InfoHoverDescriptor> descriptors) {
 		this.descriptors= descriptors;
 		this.instantiatedHovers= new ArrayList<>(descriptors.size());
 	}
 	
 	
 	@Override
-	public Object getHoverInfo(final AssistInvocationContext context) {
+	public @Nullable Object getHoverInfo(final AssistInvocationContext context,
+			final ProgressMonitor m) throws StatusException {
 		this.bestHover= null;
-		if (this.descriptors == null) {
+		final List<InfoHoverDescriptor> descriptors;
+		final List<InfoHover> instantiatedHovers;
+		synchronized (this) {
+			descriptors= this.descriptors;
+			instantiatedHovers= this.instantiatedHovers;
+		}
+		if (descriptors == null) {
 			return null;
 		}
 		
-		for (int i= 0; i < this.descriptors.size(); i++) {
-			if (i == this.instantiatedHovers.size()) {
-				this.instantiatedHovers.add(this.descriptors.get(i).createHover());
+		for (int i= 0; i < descriptors.size(); i++) {
+			m.setWorkRemaining(descriptors.size() - i);
+			if (m.isCanceled()) {
+				throw new StatusException(Statuses.CANCEL_STATUS);
 			}
-			final IInfoHover hover= this.instantiatedHovers.get(i);
+			
+			if (i == instantiatedHovers.size()) {
+				instantiatedHovers.add(descriptors.get(i).createHover());
+			}
+			final InfoHover hover= instantiatedHovers.get(i);
 			if (hover != null) {
-				final Object info= hover.getHoverInfo(context);
-				if (info != null) {
-					this.bestHover= hover;
-					return info;
+				try {
+					final Object info= hover.getHoverInfo(context, m.newSubMonitor(1));
+					if (info != null) {
+						this.bestHover= hover;
+						return info;
+					}
+				}
+				catch (final StatusException e) {
 				}
 			}
 		}
@@ -66,9 +89,10 @@
 	}
 	
 	@Override
-	public IInformationControlCreator getHoverControlCreator() {
-		if (this.bestHover != null) {
-			return this.bestHover.getHoverControlCreator();
+	public @Nullable IInformationControlCreator getHoverControlCreator() {
+		final InfoHover hover= this.bestHover;
+		if (hover != null) {
+			return hover.getHoverControlCreator();
 		}
 		return null;
 	}
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/IInfoHover.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/InfoHover.java
similarity index 76%
rename from ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/IInfoHover.java
rename to ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/InfoHover.java
index 08c14b2..de25dbd 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/IInfoHover.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/InfoHover.java
@@ -18,6 +18,11 @@
 import org.eclipse.jface.text.ITextHoverExtension;
 import org.eclipse.jface.text.ITextHoverExtension2;
 
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+import org.eclipse.statet.jcommons.status.ProgressMonitor;
+import org.eclipse.statet.jcommons.status.StatusException;
+
 import org.eclipse.statet.ecommons.ui.util.InformationDispatchHandler;
 
 
@@ -27,7 +32,8 @@
  * Intend to be implemented by client and registered to the extension point
  * <code>org.eclipse.statet.ltk.advancedInfoHover</code>.
  */
-public interface IInfoHover {
+@NonNullByDefault
+public interface InfoHover {
 	
 	
 	int MODE_TOOLTIP= InformationDispatchHandler.MODE_TOOLTIP;
@@ -37,13 +43,15 @@
 	
 	
 	/**
+	 * @param m optional progress monitor
 	 * @see ITextHoverExtension2#getHoverInfo2(org.eclipse.jface.text.ITextViewer, org.eclipse.jface.text.IRegion)
 	 */
-	Object getHoverInfo(AssistInvocationContext context);
+	@Nullable Object getHoverInfo(AssistInvocationContext context,
+			ProgressMonitor m) throws StatusException;
 	
 	/**
 	 * @see ITextHoverExtension#getHoverControlCreator()
 	 */
-	IInformationControlCreator getHoverControlCreator();
+	@Nullable IInformationControlCreator getHoverControlCreator();
 	
 }
diff --git a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/InfoHoverDescriptor.java b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/InfoHoverDescriptor.java
index 6dc6249..1ced077 100644
--- a/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/InfoHoverDescriptor.java
+++ b/ltk/org.eclipse.statet.ltk.ui/src/org/eclipse/statet/ltk/ui/sourceediting/assist/InfoHoverDescriptor.java
@@ -20,6 +20,9 @@
 import org.eclipse.core.runtime.Status;
 import org.eclipse.ui.statushandlers.StatusManager;
 
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
 import org.eclipse.statet.internal.ltk.ui.AdvancedExtensionsInternal;
 import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin;
 
@@ -27,6 +30,7 @@
 /**
  * Describes an configured information hover type.
  */
+@NonNullByDefault
 public class InfoHoverDescriptor {
 	
 	
@@ -69,16 +73,16 @@
 	 *
 	 * @return the text hover
 	 */
-	public IInfoHover createHover() {
+	public @Nullable InfoHover createHover() {
 		try {
-			return (IInfoHover) this.configurationElement.createExecutableExtension(
+			return (InfoHover) this.configurationElement.createExecutableExtension(
 					AdvancedExtensionsInternal.CONFIG_CLASS_ATTRIBUTE_NAME);
 		}
 		catch (final CoreException e) {
 			StatusManager.getManager().handle(new Status(IStatus.ERROR, LTKUIPlugin.BUNDLE_ID, 0,
 					"Could not create text hover '" + this.name + "'.", e ));
+			return null;
 		}
-		return null;
 	}
 	
 	/**
@@ -106,7 +110,7 @@
 	}
 	
 	@Override
-	public boolean equals(final Object obj) {
+	public boolean equals(final @Nullable Object obj) {
 		return (obj != null && getClass().equals(obj.getClass())
 				&& this.id.equals(((InfoHoverDescriptor) obj).getId()) );
 	}