/* * Copyright (c) 2012, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.google.dart.tools.ui.internal.text.dart; import com.google.dart.tools.ui.DartToolsPlugin; import com.google.dart.tools.ui.Messages; import com.google.dart.tools.ui.PreferenceConstants; 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.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.Link; import org.eclipse.ui.dialogs.PreferencesUtil; 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; /** * A registry for all extensions to the * <code>com.google.dart.tools.ui.dartCompletionProposalComputer</code> extension point. */ @SuppressWarnings({"rawtypes", "unchecked"}) public final class CompletionProposalComputerRegistry { private static final String EXTENSION_POINT = "dartCompletionProposalComputer"; //$NON-NLS-1$ private static final boolean OK_TO_ANNOY_USER = false; /** The singleton instance. */ private static CompletionProposalComputerRegistry fgSingleton = null; /** * Returns the default computer registry. * <p> * TODO keep this or add some other singleton, e.g. DartToolsPlugin? * </p> * * @return the singleton instance */ public static synchronized CompletionProposalComputerRegistry getDefault() { 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<CompletionProposalComputerDescriptor>}). */ private final Map fDescriptorsByPartition = new HashMap(); /** * Unmodifiable versions of the sets stored in <code>fDescriptorsByPartition</code> (key type: * {@link String}, value type: {@linkplain List List<CompletionProposalComputerDescriptor>} * ). */ private final Map fPublicDescriptorsByPartition = new HashMap(); /** * All descriptors (element type: {@link CompletionProposalComputerDescriptor} ). */ private final List fDescriptors = new ArrayList(); /** * Unmodifiable view of <code>fDescriptors</code> */ private final List fPublicDescriptors = Collections.unmodifiableList(fDescriptors); private final List fCategories = new ArrayList(); private final List 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 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 getProposalCategories() { ensureExtensionPointRead(); return fPublicCategories; } /** * 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 elements = new ArrayList(Arrays.asList(registry.getConfigurationElementsFor( DartToolsPlugin.getPluginId(), EXTENSION_POINT))); Map map = new HashMap(); List all = new ArrayList(); List categories = getCategories(elements); for (Iterator iter = elements.iterator(); iter.hasNext();) { IConfigurationElement element = (IConfigurationElement) iter.next(); try { CompletionProposalComputerDescriptor desc = new CompletionProposalComputerDescriptor( element, this, categories); Set partitions = desc.getPartitions(); for (Iterator it = partitions.iterator(); it.hasNext();) { String partition = (String) it.next(); List list = (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. */ Object[] args = {element.toString()}; String message = Messages.format( DartTextMessages.CompletionProposalComputerRegistry_invalid_message, args); IStatus status = new Status( IStatus.WARNING, DartToolsPlugin.getPluginId(), IStatus.OK, message, x); informUser(status); } } synchronized (this) { fCategories.clear(); fCategories.addAll(categories); Set partitions = map.keySet(); fDescriptorsByPartition.keySet().retainAll(partitions); fPublicDescriptorsByPartition.keySet().retainAll(partitions); for (Iterator it = partitions.iterator(); it.hasNext();) { String partition = (String) it.next(); List old = (List) fDescriptorsByPartition.get(partition); List current = (List) 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); } } /** * 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 com.google.dart.tools.ui.text.dart.IDartCompletionProposalComputer}. 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 getProposalComputerDescriptors() { ensureExtensionPointRead(); return fPublicDescriptors; } /** * 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 com.google.dart.tools.ui.text.DartPartitions} 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 com.google.dart.tools.ui.text.dart.IDartCompletionProposalComputer}. 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 getProposalComputerDescriptors(String partition) { ensureExtensionPointRead(); List result = (List) fPublicDescriptorsByPartition.get(partition); return result != null ? result : Collections.EMPTY_LIST; } /** * 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) { DartToolsPlugin.log(status); if (OK_TO_ANNOY_USER) { informUserModal(descriptor, status); } } /** * 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(); } } /** * Returns the names of contributors affected by disabling a category. * * @param category the category that would be disabled * @param culprit the culprit 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 getAffectedContributors(CompletionProposalCategory category, IContributor culprit) { Set affectedPlugins = new HashSet(); for (Iterator it = getProposalComputerDescriptors().iterator(); it.hasNext();) { CompletionProposalComputerDescriptor desc = (CompletionProposalComputerDescriptor) 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 List getCategories(List elements) { IPreferenceStore store = DartToolsPlugin.getDefault().getPreferenceStore(); String preference = store.getString(PreferenceConstants.CODEASSIST_EXCLUDED_CATEGORIES); Set disabled = new HashSet(); StringTokenizer tok = new StringTokenizer(preference, "\0"); //$NON-NLS-1$ while (tok.hasMoreTokens()) { disabled.add(tok.nextToken()); } Map 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, new Integer(rank)); } List categories = new ArrayList(); for (Iterator iter = elements.iterator(); iter.hasNext();) { IConfigurationElement element = (IConfigurationElement) 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 = (Integer) 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. */ Object[] args = {element.toString()}; String message = Messages.format( DartTextMessages.CompletionProposalComputerRegistry_invalid_message, args); IStatus status = new Status( IStatus.WARNING, DartToolsPlugin.getPluginId(), IStatus.OK, message, x); informUser(status); } } return categories; } private void informUser(IStatus status) { DartToolsPlugin.log(status); String title = DartTextMessages.CompletionProposalComputerRegistry_error_dialog_title; String message = status.getMessage(); MessageDialog.openError(DartToolsPlugin.getActiveWorkbenchShell(), title, message); } private void informUserModal(CompletionProposalComputerDescriptor descriptor, IStatus status) { String title = DartTextMessages.CompletionProposalComputerRegistry_error_dialog_title; CompletionProposalCategory category = descriptor.getCategory(); IContributor culprit = descriptor.getContributor(); Set affectedPlugins = getAffectedContributors(category, culprit); final String avoidHint; final String culpritName = culprit == null ? null : culprit.getName(); if (affectedPlugins.isEmpty()) { avoidHint = Messages.format( DartTextMessages.CompletionProposalComputerRegistry_messageAvoidanceHint, new Object[] {culpritName, category.getDisplayName()}); } else { avoidHint = Messages.format( DartTextMessages.CompletionProposalComputerRegistry_messageAvoidanceHintWithWarning, new Object[] {culpritName, category.getDisplayName(), toString(affectedPlugins)}); } String message = status.getMessage(); // inlined from MessageDialog.openError MessageDialog dialog = new MessageDialog( DartToolsPlugin.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(), "com.google.dart.tools.ui.internal.preferences.CodeAssistPreferenceAdvanced", null, null).open(); //$NON-NLS-1$ } }); GridData gridData = new GridData(SWT.FILL, SWT.BEGINNING, true, false); gridData.widthHint = this.getMinimumMessageWidth(); link.setLayoutData(gridData); return link; } }; dialog.open(); } private Object toString(Collection collection) { // strip brackets off AbstractCollection.toString() String string = collection.toString(); return string.substring(1, string.length() - 1); } }