diff options
author | Christoph Läubrich | 2021-01-21 13:58:55 +0000 |
---|---|---|
committer | Mickael Istria | 2021-01-22 13:05:16 +0000 |
commit | 02497e3b6625f487edaa64d0276cb77e29d9af42 (patch) | |
tree | 19f32e76b0bfd7f96cf1387b05cd0b6f4577a6ba | |
parent | 0ff22b3f712a6838f4d1c5b31656e0df1eeb122a (diff) | |
download | eclipse.platform.text-02497e3b6625f487edaa64d0276cb77e29d9af42.tar.gz eclipse.platform.text-02497e3b6625f487edaa64d0276cb77e29d9af42.tar.xz eclipse.platform.text-02497e3b6625f487edaa64d0276cb77e29d9af42.zip |
Bug 570459 - [genericeditor] Support ContentAssistProcessors to be
registered as OSGi-Services
Change-Id: Ie642d4f401646ff2b186166ec3a5c9ef484f0e4f
Signed-off-by: Christoph Läubrich <laeubi@laeubi-soft.de>
5 files changed, 381 insertions, 37 deletions
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 2f92d9c4906..3c4a8349dca 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 @@ public class ContentAssistant implements IContentAssistant, IContentAssistantExt 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 @@ public class ContentAssistant implements IContentAssistant, IContentAssistantExt 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 dd18ceaf298..dcf4a7d27c0 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 static org.junit.Assert.assertTrue; 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.IStatus; 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 @@ public class CompletionTest extends AbstratGenericEditorTest { } @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 @@ public class CompletionTest extends AbstratGenericEditorTest { } } + + 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 00000000000..d2d5b9176a7 --- /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 41ae32499e3..2ef26c14123 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.IAdaptable; 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.IQuickAssistProcessor; 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 @@ public final class ExtensionBasedTextViewerConfiguration extends TextSourceViewe private Set<IContentType> contentTypes; private IDocument document; - private ContentAssistant contentAssistant; - private List<IContentAssistProcessor> processors; + private GenericEditorContentAssistant contentAssistant; /** * @@ -85,7 +82,7 @@ public final class ExtensionBasedTextViewerConfiguration extends TextSourceViewe 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 @@ public final class ExtensionBasedTextViewerConfiguration extends TextSourceViewe 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 @@ public final class ExtensionBasedTextViewerConfiguration extends TextSourceViewe @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 @@ public final class ExtensionBasedTextViewerConfiguration extends TextSourceViewe } 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 @@ public final class ExtensionBasedTextViewerConfiguration extends TextSourceViewe 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 00000000000..62953a95310 --- /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(); + } +} |