Bug 570459 - [genericeditor] Support ContentAssistProcessors to be
registered as OSGi-Services

Change-Id: Ie642d4f401646ff2b186166ec3a5c9ef484f0e4f
Signed-off-by: Christoph Läubrich <laeubi@laeubi-soft.de>
diff --git a/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/ContentAssistant.java b/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/ContentAssistant.java
index 2f92d9c..3c4a834 100644
--- a/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/ContentAssistant.java
+++ b/org.eclipse.jface.text/src/org/eclipse/jface/text/contentassist/ContentAssistant.java
@@ -16,6 +16,7 @@
  *     John Glassmyer, jogl@google.com - catch Content Assist exceptions to protect navigation keys - http://bugs.eclipse.org/434901
  *     Mickael Istria (Red Hat Inc.) - [251156] Allow multiple contentAssitProviders internally & inheritance
  *     Christoph Läubrich - Bug 508821 - [Content assist] More flexible API in IContentAssistProcessor to decide whether to auto-activate or not
+ *     						Bug 570459 - [genericeditor] Support ContentAssistProcessors to be registered as OSGi-Services
  *******************************************************************************/
 package org.eclipse.jface.text.contentassist;
 
@@ -1131,10 +1132,22 @@
 		if (processor == null) {
 			fProcessors.remove(contentType);
 		} else {
-			if (!fProcessors.containsKey(contentType)) {
-				fProcessors.put(contentType, new LinkedHashSet<>());
-			}
-			fProcessors.get(contentType).add(processor);
+			fProcessors.computeIfAbsent(contentType, key -> new LinkedHashSet<>()).add(processor);
+		}
+	}
+
+	/**
+	 * removes the given processor from all content types in this {@link ContentAssistant}
+	 *
+	 * @param processor The content-assist process to remove
+	 * @since 3.17
+	 */
+	public void removeContentAssistProcessor(IContentAssistProcessor processor) {
+		if (fProcessors == null || processor == null) {
+			return;
+		}
+		for (Set<IContentAssistProcessor> set : fProcessors.values()) {
+			set.remove(processor);
 		}
 	}
 
@@ -2705,4 +2718,5 @@
 	boolean isAutoActivation() {
 		return fIsAutoActivated;
 	}
+
 }
diff --git a/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/CompletionTest.java b/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/CompletionTest.java
index dd18cea..dcf4a7d 100644
--- a/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/CompletionTest.java
+++ b/org.eclipse.ui.genericeditor.tests/src/org/eclipse/ui/genericeditor/tests/CompletionTest.java
@@ -20,12 +20,18 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Hashtable;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.junit.After;
 import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceRegistration;
 
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.ST;
@@ -45,7 +51,11 @@
 import org.eclipse.core.runtime.Platform;
 
 import org.eclipse.jface.text.ITextSelection;
+import org.eclipse.jface.text.ITextViewer;
 import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.jface.text.contentassist.IContextInformationValidator;
 import org.eclipse.jface.text.tests.util.DisplayHelper;
 
 import org.eclipse.ui.genericeditor.tests.contributions.BarContentAssistProcessor;
@@ -87,6 +97,26 @@
 	}
 
 	@Test
+	public void testCompletionService() throws Exception {
+		Bundle bundle= FrameworkUtil.getBundle(CompletionTest.class);
+		assertNotNull(bundle);
+		BundleContext bundleContext= bundle.getBundleContext();
+		assertNotNull(bundleContext);
+		MockContentAssistProcessor service= new MockContentAssistProcessor();
+		ServiceRegistration<IContentAssistProcessor> registration= bundleContext.registerService(IContentAssistProcessor.class, service,
+				new Hashtable<>(Collections.singletonMap("contentType", "org.eclipse.ui.genericeditor.tests.content-type")));
+		DisplayHelper.driveEventQueue(Display.getCurrent());
+		final Set<Shell> beforeShells= Arrays.stream(editor.getSite().getShell().getDisplay().getShells()).filter(Shell::isVisible).collect(Collectors.toSet());
+		editor.selectAndReveal(3, 0);
+		openConentAssist();
+		this.completionShell= findNewShell(beforeShells, editor.getSite().getShell().getDisplay());
+		final Table completionProposalList= findCompletionSelectionControl(completionShell);
+		checkCompletionContent(completionProposalList);
+		assertTrue("Service was not called!", service.called);
+		registration.unregister();
+	}
+
+	@Test
 	public void testCompletionUsingViewerSelection() throws Exception {
 		final Set<Shell> beforeShells = Arrays.stream(editor.getSite().getShell().getDisplay().getShells()).filter(Shell::isVisible).collect(Collectors.toSet());
 		editor.getDocumentProvider().getDocument(editor.getEditorInput()).set("abc");
@@ -257,4 +287,41 @@
 		}
 
 	}
+
+	private static final class MockContentAssistProcessor implements IContentAssistProcessor {
+
+		private boolean called;
+
+		@Override
+		public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
+			called= true;
+			return null;
+		}
+
+		@Override
+		public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
+			return null;
+		}
+
+		@Override
+		public char[] getCompletionProposalAutoActivationCharacters() {
+			return null;
+		}
+
+		@Override
+		public char[] getContextInformationAutoActivationCharacters() {
+			return null;
+		}
+
+		@Override
+		public String getErrorMessage() {
+			return null;
+		}
+
+		@Override
+		public IContextInformationValidator getContextInformationValidator() {
+			return null;
+		}
+
+	}
 }
diff --git a/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/ContentTypeRelatedExtensionTracker.java b/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/ContentTypeRelatedExtensionTracker.java
new file mode 100644
index 0000000..d2d5b91
--- /dev/null
+++ b/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/ContentTypeRelatedExtensionTracker.java
@@ -0,0 +1,137 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Christoph Läubrich 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:
+ * Christoph Läubrich - Inital API and implementation
+ *******************************************************************************/
+package org.eclipse.ui.internal.genericeditor;
+
+import java.util.Collection;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.internal.genericeditor.ContentTypeRelatedExtensionTracker.LazyServiceSupplier;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+/**
+ * {@link ServiceTrackerCustomizer} that maps OSGi-Services to
+ * ContentTypeRelatedExtensions
+ *
+ * @param <T> the type of extension to map
+ */
+final class ContentTypeRelatedExtensionTracker<T> implements ServiceTrackerCustomizer<T, LazyServiceSupplier<T>> {
+
+	private BundleContext bundleContext;
+	private ServiceTracker<T, LazyServiceSupplier<T>> serviceTracker;
+	private Consumer<LazyServiceSupplier<T>> addAction;
+	private Consumer<LazyServiceSupplier<T>> removeAction;
+	private Display display;
+
+	ContentTypeRelatedExtensionTracker(BundleContext bundleContext, Class<T> serviceType, Display display) {
+		this.bundleContext = bundleContext;
+		this.display = display;
+		serviceTracker = new ServiceTracker<>(bundleContext, serviceType, this);
+	}
+
+	public void stopTracking() {
+		serviceTracker.close();
+	}
+
+	@Override
+	public LazyServiceSupplier<T> addingService(ServiceReference<T> reference) {
+		LazyServiceSupplier<T> supplier = new LazyServiceSupplier<>(bundleContext, reference);
+		if (addAction != null) {
+			display.asyncExec(() -> addAction.accept(supplier));
+		}
+		return supplier;
+	}
+
+	@Override
+	public void modifiedService(ServiceReference<T> reference, LazyServiceSupplier<T> service) {
+		service.update();
+	}
+
+	@Override
+	public void removedService(ServiceReference<T> reference, LazyServiceSupplier<T> service) {
+		service.dispose();
+		if (removeAction != null) {
+			display.asyncExec(() -> removeAction.accept(service));
+		}
+	}
+
+	public void onAdd(Consumer<LazyServiceSupplier<T>> action) {
+		this.addAction = action;
+	}
+
+	public void onRemove(Consumer<LazyServiceSupplier<T>> action) {
+		this.removeAction = action;
+	}
+
+	public void startTracking() {
+		serviceTracker.open();
+	}
+
+	public Collection<LazyServiceSupplier<T>> getTracked() {
+		return serviceTracker.getTracked().values();
+	}
+
+	public static final class LazyServiceSupplier<S> implements Supplier<S> {
+		private ServiceReference<S> reference;
+		private BundleContext bundleContext;
+		private boolean disposed;
+		private S serviceObject;
+		private IContentType contentType;
+
+		LazyServiceSupplier(BundleContext bundleContext, ServiceReference<S> reference) {
+			this.reference = reference;
+			this.bundleContext = bundleContext;
+			update();
+		}
+
+		private String getProperty(String attribute) {
+			return (String) reference.getProperty(attribute);
+		}
+
+		synchronized void update() {
+			contentType = Platform.getContentTypeManager()
+					.getContentType(getProperty(GenericContentTypeRelatedExtension.CONTENT_TYPE_ATTRIBUTE));
+		}
+
+		public synchronized IContentType getContentType() {
+			return contentType;
+		}
+
+		synchronized void dispose() {
+			disposed = true;
+			if (serviceObject != null) {
+				bundleContext.ungetService(reference);
+			}
+		}
+
+		@Override
+		public synchronized S get() {
+			if (!disposed && serviceObject == null) {
+				serviceObject = bundleContext.getService(reference);
+			}
+			return serviceObject;
+		}
+
+		public synchronized boolean isPresent() {
+			return serviceObject != null;
+		}
+	}
+
+}
diff --git a/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/ExtensionBasedTextViewerConfiguration.java b/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/ExtensionBasedTextViewerConfiguration.java
index 41ae324..2ef26c1 100644
--- a/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/ExtensionBasedTextViewerConfiguration.java
+++ b/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/ExtensionBasedTextViewerConfiguration.java
@@ -14,6 +14,7 @@
  *                               - Bug 521382 default highlight reconciler
  *   Simon Scholz <simon.scholz@vogella.com> - Bug 527830
  *   Angelo Zerr <angelo.zerr@gmail.com> - [generic editor] Default Code folding for generic editor should use IndentFoldingStrategy - Bug 520659
+ *   Christoph Läubrich - Bug 570459 - [genericeditor] Support ContentAssistProcessors to be registered as OSGi-Services
  *******************************************************************************/
 package org.eclipse.ui.internal.genericeditor;
 
@@ -33,14 +34,12 @@
 import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.content.IContentType;
 import org.eclipse.jface.preference.IPreferenceStore;
-import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
 import org.eclipse.jface.text.DefaultInformationControl;
 import org.eclipse.jface.text.IAutoEditStrategy;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IDocumentPartitioningListener;
-import org.eclipse.jface.text.IInformationControl;
 import org.eclipse.jface.text.ITextHover;
-import org.eclipse.jface.text.contentassist.ContentAssistant;
+import org.eclipse.jface.text.ITextViewer;
 import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
 import org.eclipse.jface.text.contentassist.IContentAssistant;
 import org.eclipse.jface.text.presentation.IPresentationReconciler;
@@ -49,7 +48,6 @@
 import org.eclipse.jface.text.quickassist.QuickAssistAssistant;
 import org.eclipse.jface.text.reconciler.IReconciler;
 import org.eclipse.jface.text.source.ISourceViewer;
-import org.eclipse.swt.widgets.Shell;
 import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
 import org.eclipse.ui.internal.editors.text.EditorsPlugin;
 import org.eclipse.ui.internal.genericeditor.folding.DefaultFoldingReconciler;
@@ -72,8 +70,7 @@
 	private Set<IContentType> contentTypes;
 	private IDocument document;
 
-	private ContentAssistant contentAssistant;
-	private List<IContentAssistProcessor> processors;
+	private GenericEditorContentAssistant contentAssistant;
 
 	/**
 	 *
@@ -85,7 +82,7 @@
 		this.editor = editor;
 	}
 
-	Set<IContentType> getContentTypes(ISourceViewer viewer) {
+	Set<IContentType> getContentTypes(ITextViewer viewer) {
 		if (this.contentTypes == null) {
 			this.contentTypes = new LinkedHashSet<>();
 			String fileName = null;
@@ -107,7 +104,7 @@
 		return this.contentTypes;
 	}
 
-	private String getCurrentFileName(ISourceViewer viewer) {
+	private String getCurrentFileName(ITextViewer viewer) {
 		String fileName = null;
 		if (this.editor != null) {
 			fileName = editor.getEditorInput().getName();
@@ -140,28 +137,15 @@
 	@Override
 	public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) {
 		ContentAssistProcessorRegistry registry = GenericEditorPlugin.getDefault().getContentAssistProcessorRegistry();
-		contentAssistant = new ContentAssistant(true);
-		contentAssistant.setContextInformationPopupOrientation(IContentAssistant.CONTEXT_INFO_BELOW);
-		contentAssistant.setProposalPopupOrientation(IContentAssistant.PROPOSAL_REMOVE);
-		contentAssistant.setAutoActivationDelay(0);
-		contentAssistant.enableColoredLabels(true);
-		contentAssistant.enableAutoActivation(true);
-		this.processors = registry.getContentAssistProcessors(sourceViewer, editor, getContentTypes(sourceViewer));
-		if (this.processors.isEmpty()) {
-			this.processors.add(new DefaultContentAssistProcessor());
-		}
-		for (IContentAssistProcessor processor : this.processors) {
-			contentAssistant.addContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);
-		}
+		ContentTypeRelatedExtensionTracker<IContentAssistProcessor> contentAssistProcessorTracker = new ContentTypeRelatedExtensionTracker<>(
+				GenericEditorPlugin.getDefault().getBundle().getBundleContext(), IContentAssistProcessor.class,
+				sourceViewer.getTextWidget().getDisplay());
+		Set<IContentType> types = getContentTypes(sourceViewer);
+		contentAssistant = new GenericEditorContentAssistant(contentAssistProcessorTracker,
+				registry.getContentAssistProcessors(sourceViewer, editor, types), types);
 		if (this.document != null) {
 			associateTokenContentTypes(this.document);
 		}
-		contentAssistant.setInformationControlCreator(new AbstractReusableInformationControlCreator() {
-			@Override
-			protected IInformationControl doCreateInformationControl(Shell parent) {
-				return new DefaultInformationControl(parent);
-			}
-		});
 		watchDocument(sourceViewer.getDocument());
 		return contentAssistant;
 	}
@@ -197,14 +181,10 @@
 	}
 
 	private void associateTokenContentTypes(IDocument document) {
-		if (contentAssistant == null || this.processors == null) {
+		if (contentAssistant == null) {
 			return;
 		}
-		for (String legalTokenContentType : document.getLegalContentTypes()) {
-			for (IContentAssistProcessor processor : this.processors) {
-				contentAssistant.addContentAssistProcessor(processor, legalTokenContentType);
-			}
-		}
+		contentAssistant.updateTokens(document);
 	}
 
 	@Override
@@ -268,4 +248,5 @@
 		targets.put(ExtensionBasedTextEditor.GENERIC_EDITOR_ID, editor);
 		return targets;
 	}
+
 }
diff --git a/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/GenericEditorContentAssistant.java b/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/GenericEditorContentAssistant.java
new file mode 100644
index 0000000..62953a9
--- /dev/null
+++ b/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/GenericEditorContentAssistant.java
@@ -0,0 +1,145 @@
+/*******************************************************************************
+ * Copyright (c) 2021 Christoph Läubrich 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:
+ * Christoph Läubrich - Inital API and implementation
+ *******************************************************************************/
+package org.eclipse.ui.internal.genericeditor;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.jface.text.AbstractReusableInformationControlCreator;
+import org.eclipse.jface.text.DefaultInformationControl;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IInformationControl;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.contentassist.ContentAssistant;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.contentassist.IContentAssistant;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Extension of the ContentAssistant that supports the following additional
+ * features:
+ * <ul>
+ * <li>#updateTokens refresh the registration of
+ * {@link IContentAssistProcessor}s on document changes</li>
+ * <li>Using a ContentTypeRelatedExtensionTracker to dynamically track
+ * IContentAssistProcessor in the OSGi service registry
+ * </ul>
+ * 
+ * @author christoph
+ *
+ */
+public class GenericEditorContentAssistant extends ContentAssistant {
+	private static final DefaultContentAssistProcessor DEFAULT_CONTENT_ASSIST_PROCESSOR = new DefaultContentAssistProcessor();
+	private ContentTypeRelatedExtensionTracker<IContentAssistProcessor> contentAssistProcessorTracker;
+	private Set<IContentType> types;
+	private List<IContentAssistProcessor> processors;
+
+	/**
+	 * Creates a new GenericEditorContentAssistant instance for the given content
+	 * types and contentAssistProcessorTracker
+	 * 
+	 * @param contentAssistProcessorTracker the tracker to use for tracking
+	 *                                      additional
+	 *                                      {@link IContentAssistProcessor}s in the
+	 *                                      OSGi service factory
+	 * @param processors                    the static processor list
+	 * @param types                         the {@link IContentType} that are used
+	 *                                      to filter appropriate candidates from
+	 *                                      the registry
+	 */
+	public GenericEditorContentAssistant(
+			ContentTypeRelatedExtensionTracker<IContentAssistProcessor> contentAssistProcessorTracker,
+			List<IContentAssistProcessor> processors, Set<IContentType> types) {
+		super(true);
+		this.contentAssistProcessorTracker = contentAssistProcessorTracker;
+		this.processors = Objects.requireNonNullElseGet(processors, () -> Collections.emptyList());
+		this.types = types;
+
+		setContextInformationPopupOrientation(IContentAssistant.CONTEXT_INFO_BELOW);
+		setProposalPopupOrientation(IContentAssistant.PROPOSAL_REMOVE);
+		setAutoActivationDelay(0);
+		enableColoredLabels(true);
+		enableAutoActivation(true);
+		setInformationControlCreator(new AbstractReusableInformationControlCreator() {
+			@Override
+			protected IInformationControl doCreateInformationControl(Shell parent) {
+				return new DefaultInformationControl(parent);
+			}
+		});
+	}
+
+	/**
+	 * Updates the {@link IContentAssistProcessor} registrations according to the
+	 * documents content-type tokens
+	 * 
+	 * @param document the document to use for updating the tokens
+	 */
+	public void updateTokens(IDocument document) {
+		updateProcessors(document);
+		contentAssistProcessorTracker.getTracked().stream().filter(s -> s.isPresent()).map(s -> s.get())
+				.forEach(p -> updateProcessorToken(p, document));
+	}
+
+	private void updateProcessors(IDocument iDocument) {
+		if (processors.isEmpty()) {
+			updateProcessorToken(DEFAULT_CONTENT_ASSIST_PROCESSOR, iDocument);
+		} else {
+			for (IContentAssistProcessor processor : processors) {
+				updateProcessorToken(processor, iDocument);
+			}
+		}
+	}
+
+	private void updateProcessorToken(IContentAssistProcessor processor, IDocument document) {
+		removeContentAssistProcessor(processor);
+		addContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE);
+		if (document != null) {
+			for (String contentType : document.getLegalContentTypes()) {
+				addContentAssistProcessor(processor, contentType);
+			}
+		}
+		if (processor != DEFAULT_CONTENT_ASSIST_PROCESSOR) {
+			removeContentAssistProcessor(DEFAULT_CONTENT_ASSIST_PROCESSOR);
+		}
+	}
+
+	@Override
+	public void uninstall() {
+		contentAssistProcessorTracker.stopTracking();
+		super.uninstall();
+	}
+
+	@Override
+	public void install(ITextViewer textViewer) {
+		super.install(textViewer);
+		updateProcessors(textViewer.getDocument());
+		contentAssistProcessorTracker.onAdd(added -> {
+			if (types.contains(added.getContentType())) {
+				IContentAssistProcessor processor = added.get();
+				if (processor != null) {
+					updateProcessorToken(processor, textViewer.getDocument());
+				}
+			}
+		});
+		contentAssistProcessorTracker.onRemove(removed -> {
+			if (removed.isPresent()) {
+				removeContentAssistProcessor(removed.get());
+			}
+		});
+		contentAssistProcessorTracker.startTracking();
+	}
+}