blob: 6b51724ec046134e0d228a8906e4c0985093fa41 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2017 IBM Corporation and others.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
*
*******************************************************************************/
package org.eclipse.dltk.ui.text.completion;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.IExtensionRegistry;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.dltk.ui.DLTKUIPlugin;
import org.eclipse.dltk.ui.PreferenceConstants;
import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Link;
import org.eclipse.ui.dialogs.PreferencesUtil;
public final class CompletionProposalComputerRegistry {
private static final String EXTENSION_POINT = "scriptCompletionProposalComputer"; //$NON-NLS-1$
/** The singleton instance. */
private static CompletionProposalComputerRegistry fgSingleton = null;
/**
* Returns the default computer registry.
* <p>
* TODO keep this or add some other singleton, e.g. JavaPlugin?
* </p>
*
* @return the singleton instance
*/
public static CompletionProposalComputerRegistry getDefault() {
if (fgSingleton == null) {
synchronized (CompletionProposalComputerRegistry.class) {
if (fgSingleton == null) {
fgSingleton = new CompletionProposalComputerRegistry();
}
}
}
return fgSingleton;
}
/**
* The sets of descriptors, grouped by partition type (key type: {@link String},
* value type: {@linkplain List
* List&lt;CompletionProposalComputerDescriptor&gt;}).
*/
private final Map<String, List<CompletionProposalComputerDescriptor>> fDescriptorsByPartition = new HashMap<>();
/**
* Unmodifiable versions of the sets stored in
* <code>fDescriptorsByPartition</code> (key type: {@link String}, value type:
* {@linkplain List List&lt;CompletionProposalComputerDescriptor&gt;} ).
*/
private final Map<String, List<CompletionProposalComputerDescriptor>> fPublicDescriptorsByPartition = new HashMap<>();
/**
* All descriptors (element type: {@link CompletionProposalComputerDescriptor}).
*/
private final List<CompletionProposalComputerDescriptor> fDescriptors = new ArrayList<>();
/**
* Unmodifiable view of <code>fDescriptors</code>
*/
private final List<CompletionProposalComputerDescriptor> fPublicDescriptors = Collections
.unmodifiableList(fDescriptors);
private final List<CompletionProposalCategory> fCategories = new ArrayList<>();
private final List<CompletionProposalCategory> fPublicCategories = Collections.unmodifiableList(fCategories);
/**
* <code>true</code> if this registry has been loaded.
*/
private boolean fLoaded = false;
/**
* Creates a new instance.
*/
public CompletionProposalComputerRegistry() {
}
/**
* Returns the list of {@link CompletionProposalComputerDescriptor}s describing
* all extensions to the <code>javaCompletionProposalComputer</code> extension
* point for the given partition type.
* <p>
* A valid partition is either one of the constants defined in
* {@link org.eclipse.dltk.ui.text.IJavaPartitions} or
* {@link org.eclipse.jface.text.IDocument#DEFAULT_CONTENT_TYPE}. An empty list
* is returned if there are no extensions for the given partition.
* </p>
* <p>
* The returned list is read-only and is sorted in the order that the extensions
* were read in. There are no duplicate elements in the returned list. The
* returned list may change if plug-ins are loaded or unloaded while the
* application is running or if an extension violates the API contract of
* {@link org.eclipse.dltk.ui.text.java.IScriptCompletionProposalComputer}. When
* computing proposals, it is therefore imperative to copy the returned list
* before iterating over it.
* </p>
*
* @param partition the partition type for which to retrieve the computer
* descriptors
* @return the list of extensions to the
* <code>javaCompletionProposalComputer</code> extension point (element
* type: {@link CompletionProposalComputerDescriptor})
*/
List<CompletionProposalComputerDescriptor> getProposalComputerDescriptors(String partition) {
ensureExtensionPointRead();
List<CompletionProposalComputerDescriptor> result = fPublicDescriptorsByPartition.get(partition);
return result != null ? result : Collections.<CompletionProposalComputerDescriptor>emptyList();
}
/**
* Returns the list of {@link CompletionProposalComputerDescriptor}s describing
* all extensions to the <code>javaCompletionProposalComputer</code> extension
* point.
* <p>
* The returned list is read-only and is sorted in the order that the extensions
* were read in. There are no duplicate elements in the returned list. The
* returned list may change if plug-ins are loaded or unloaded while the
* application is running or if an extension violates the API contract of
* {@link org.eclipse.dltk.ui.text.java.IScriptCompletionProposalComputer}. When
* computing proposals, it is therefore imperative to copy the returned list
* before iterating over it.
* </p>
*
* @return the list of extensions to the
* <code>javaCompletionProposalComputer</code> extension point (element
* type: {@link CompletionProposalComputerDescriptor})
*/
List<CompletionProposalComputerDescriptor> getProposalComputerDescriptors() {
ensureExtensionPointRead();
return fPublicDescriptors;
}
/**
* Returns the list of proposal categories contributed to the
* <code>javaCompletionProposalComputer</code> extension point.
* <p>
* <p>
* The returned list is read-only and is sorted in the order that the extensions
* were read in. There are no duplicate elements in the returned list. The
* returned list may change if plug-ins are loaded or unloaded while the
* application is running.
* </p>
*
* @return list of proposal categories contributed to the
* <code>javaCompletionProposalComputer</code> extension point (element
* type: {@link CompletionProposalCategory})
*/
public List<CompletionProposalCategory> getProposalCategories() {
ensureExtensionPointRead();
return fPublicCategories;
}
/**
* Ensures that the extensions are read and stored in
* <code>fDescriptorsByPartition</code>.
*/
private void ensureExtensionPointRead() {
boolean reload;
synchronized (this) {
reload = !fLoaded;
fLoaded = true;
}
if (reload)
reload();
}
/**
* Reloads the extensions to the extension point.
* <p>
* This method can be called more than once in order to reload from a changed
* extension registry.
* </p>
*/
public void reload() {
IExtensionRegistry registry = Platform.getExtensionRegistry();
List<IConfigurationElement> elements = new ArrayList<>(
Arrays.asList(registry.getConfigurationElementsFor(DLTKUIPlugin.getPluginId(), EXTENSION_POINT)));
Map<String, List<CompletionProposalComputerDescriptor>> map = new HashMap<>();
List<CompletionProposalComputerDescriptor> all = new ArrayList<>();
List<CompletionProposalCategory> categories = getCategories(elements);
for (Iterator<IConfigurationElement> iter = elements.iterator(); iter.hasNext();) {
IConfigurationElement element = iter.next();
try {
CompletionProposalComputerDescriptor desc = new CompletionProposalComputerDescriptor(element, this,
categories);
final Set<String> partitions = desc.getPartitions();
for (String partition : partitions) {
List<CompletionProposalComputerDescriptor> list = map.get(partition);
if (list == null) {
list = new ArrayList<>();
map.put(partition, list);
}
list.add(desc);
}
all.add(desc);
} catch (InvalidRegistryObjectException x) {
/*
* Element is not valid any longer as the contributing plug-in was unloaded or
* for some other reason. Do not include the extension in the list and inform
* the user about it.
*/
String message = MessageFormat.format(
ScriptTextMessages.CompletionProposalComputerRegistry_invalid_message, element.toString());
IStatus status = new Status(IStatus.WARNING, DLTKUIPlugin.getPluginId(), IStatus.OK, message, x);
informUser(status);
}
}
synchronized (this) {
fCategories.clear();
fCategories.addAll(categories);
Set<String> partitions = map.keySet();
fDescriptorsByPartition.keySet().retainAll(partitions);
fPublicDescriptorsByPartition.keySet().retainAll(partitions);
for (Iterator<String> it = partitions.iterator(); it.hasNext();) {
String partition = it.next();
List<CompletionProposalComputerDescriptor> old = fDescriptorsByPartition.get(partition);
List<CompletionProposalComputerDescriptor> current = map.get(partition);
if (old != null) {
old.clear();
old.addAll(current);
} else {
fDescriptorsByPartition.put(partition, current);
fPublicDescriptorsByPartition.put(partition, Collections.unmodifiableList(current));
}
}
fDescriptors.clear();
fDescriptors.addAll(all);
}
}
private List<CompletionProposalCategory> getCategories(List<IConfigurationElement> elements) {
IPreferenceStore store = DLTKUIPlugin.getDefault().getPreferenceStore();
String preference = store.getString(PreferenceConstants.CODEASSIST_EXCLUDED_CATEGORIES);
Set<String> disabled = new HashSet<>();
StringTokenizer tok = new StringTokenizer(preference, "\0"); //$NON-NLS-1$
while (tok.hasMoreTokens())
disabled.add(tok.nextToken());
Map<String, Integer> ordered = new HashMap<>();
preference = store.getString(PreferenceConstants.CODEASSIST_CATEGORY_ORDER);
tok = new StringTokenizer(preference, "\0"); //$NON-NLS-1$
while (tok.hasMoreTokens()) {
StringTokenizer inner = new StringTokenizer(tok.nextToken(), ":"); //$NON-NLS-1$
String id = inner.nextToken();
int rank = Integer.parseInt(inner.nextToken());
ordered.put(id, Integer.valueOf(rank));
}
List<CompletionProposalCategory> categories = new ArrayList<>();
for (Iterator<IConfigurationElement> iter = elements.iterator(); iter.hasNext();) {
IConfigurationElement element = iter.next();
try {
if (element.getName().equals("proposalCategory")) { //$NON-NLS-1$
iter.remove(); // remove from list to leave only computers
CompletionProposalCategory category = new CompletionProposalCategory(element, this);
categories.add(category);
category.setIncluded(!disabled.contains(category.getId()));
Integer rank = ordered.get(category.getId());
if (rank != null) {
int r = rank.intValue();
boolean separate = r < 0xffff;
category.setSeparateCommand(separate);
category.setSortOrder(r);
}
}
} catch (InvalidRegistryObjectException x) {
/*
* Element is not valid any longer as the contributing plug-in was unloaded or
* for some other reason. Do not include the extension in the list and inform
* the user about it.
*/
String message = MessageFormat.format(
ScriptTextMessages.CompletionProposalComputerRegistry_invalid_message, element.toString());
IStatus status = new Status(IStatus.WARNING, DLTKUIPlugin.getPluginId(), IStatus.OK, message, x);
informUser(status);
}
}
return categories;
}
/**
* Log the status and inform the user about a misbehaving extension.
*
* @param descriptor the descriptor of the misbehaving extension
* @param status a status object that will be logged
*/
void informUser(CompletionProposalComputerDescriptor descriptor, IStatus status) {
DLTKUIPlugin.log(status);
String title = ScriptTextMessages.CompletionProposalComputerRegistry_error_dialog_title;
CompletionProposalCategory category = descriptor.getCategory();
IContributor culprit = descriptor.getContributor();
Set<String> affectedPlugins = getAffectedContributors(category, culprit);
final String avoidHint;
final String culpritName = culprit == null ? null : culprit.getName();
if (affectedPlugins.isEmpty())
avoidHint = MessageFormat.format(ScriptTextMessages.CompletionProposalComputerRegistry_messageAvoidanceHint,
culpritName, category.getDisplayName());
else
avoidHint = MessageFormat.format(
ScriptTextMessages.CompletionProposalComputerRegistry_messageAvoidanceHintWithWarning, culpritName,
category.getDisplayName(), toString(affectedPlugins));
String message = status.getMessage();
// inlined from MessageDialog.openError
Runnable openPopup = () -> {
MessageDialog dialog = new MessageDialog(DLTKUIPlugin.getActiveWorkbenchShell(), title,
null /* default image */, message, MessageDialog.ERROR, new String[] { IDialogConstants.OK_LABEL },
0) {
@Override
protected Control createCustomArea(Composite parent) {
Link link = new Link(parent, SWT.NONE);
link.setText(avoidHint);
link.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
PreferencesUtil.createPreferenceDialogOn(getShell(),
"org.eclipse.dltk.ui.preferences.CodeAssistPreferenceAdvanced", //$NON-NLS-1$
null, null).open();
}
});
GridData gridData = new GridData(SWT.FILL, SWT.BEGINNING, true, false);
gridData.widthHint = this.getMinimumMessageWidth();
link.setLayoutData(gridData);
return link;
}
};
dialog.open();
};
if (Display.getCurrent() != null) {
openPopup.run();
} else {
Display.getDefault().syncExec(openPopup);
}
}
/**
* Returns the names of contributors affected by disabling a category.
*
* @param category the category that would be disabled
* @param culprit the cuprit plug-in, which is not included in the returned
* list
* @return the names of the contributors other than <code>culprit</code> that
* contribute to <code>category</code> (element type: {@link String})
*/
private Set<String> getAffectedContributors(CompletionProposalCategory category, IContributor culprit) {
Set<String> affectedPlugins = new HashSet<>();
for (Iterator<CompletionProposalComputerDescriptor> it = getProposalComputerDescriptors().iterator(); it
.hasNext();) {
CompletionProposalComputerDescriptor desc = it.next();
CompletionProposalCategory cat = desc.getCategory();
if (cat.equals(category)) {
IContributor contributor = desc.getContributor();
if (contributor != null && !culprit.equals(contributor))
affectedPlugins.add(contributor.getName());
}
}
return affectedPlugins;
}
private Object toString(Collection<String> collection) {
// strip brackets off AbstractCollection.toString()
String string = collection.toString();
return string.substring(1, string.length() - 1);
}
private void informUser(IStatus status) {
DLTKUIPlugin.log(status);
String title = ScriptTextMessages.CompletionProposalComputerRegistry_error_dialog_title;
String message = status.getMessage();
MessageDialog.openError(DLTKUIPlugin.getActiveWorkbenchShell(), title, message);
}
}