/** * Copyright (c) 2010, 2012 Darmstadt University of Technology. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Marcel Bruch - initial API and implementation. */ package org.eclipse.recommenders.completion.rcp.processable; import static org.eclipse.recommenders.completion.rcp.CompletionContextKey.ACTIVE_PROCESSORS; import static org.eclipse.recommenders.completion.rcp.processable.ProcessableProposalFactory.create; import static org.eclipse.recommenders.completion.rcp.processable.ProposalTag.*; import static org.eclipse.recommenders.internal.completion.rcp.Constants.*; import static org.eclipse.recommenders.internal.completion.rcp.l10n.LogMessages.ERROR_SESSION_PROCESSOR_FAILED; import static org.eclipse.recommenders.utils.Checks.cast; import static org.eclipse.recommenders.utils.Logs.log; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.inject.Inject; import javax.inject.Provider; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.core.CompletionProposal; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitEditor; import org.eclipse.jdt.internal.ui.text.java.CompletionProposalCategory; import org.eclipse.jdt.internal.ui.text.java.CompletionProposalComputerRegistry; import org.eclipse.jdt.internal.ui.text.java.JavaAllCompletionProposalComputer; import org.eclipse.jdt.internal.ui.text.java.JavaMethodCompletionProposal; import org.eclipse.jdt.ui.PreferenceConstants; import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext; import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.ContentAssistEvent; import org.eclipse.jface.text.contentassist.ICompletionListener; import org.eclipse.jface.text.contentassist.ICompletionListenerExtension2; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.source.ContentAssistantFacade; import org.eclipse.jface.text.source.SourceViewer; import org.eclipse.recommenders.completion.rcp.CompletionContextKey; import org.eclipse.recommenders.completion.rcp.DisableContentAssistCategoryJob; import org.eclipse.recommenders.completion.rcp.ICompletionContextFunction; import org.eclipse.recommenders.completion.rcp.IRecommendersCompletionContext; import org.eclipse.recommenders.completion.rcp.RecommendersCompletionContext; import org.eclipse.recommenders.internal.completion.rcp.CompletionRcpPreferences; import org.eclipse.recommenders.internal.completion.rcp.EmptyCompletionProposal; import org.eclipse.recommenders.internal.completion.rcp.EnabledCompletionProposal; import org.eclipse.recommenders.rcp.IAstProvider; import org.eclipse.recommenders.rcp.SharedImages; import org.eclipse.recommenders.utils.Logs; import org.eclipse.ui.IEditorPart; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; @SuppressWarnings({ "restriction", "rawtypes" }) public class IntelligentCompletionProposalComputer extends JavaAllCompletionProposalComputer implements ICompletionListener, ICompletionListenerExtension2 { /** * A whitelist ensuring that the CompilationUnitEditor we are dealing with actually contains Java code. This is * necessary as certain plugins (Groovy Eclipse, Scala IDE) extend the JDT's CompilationUnitEditor. * * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=470372">Bug 470372</a> * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=470406">Bug 470406</a> * @see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=474318">Bug 474318</a> */ private static final List<String> JAVA_EDITOR_WHITELIST = ImmutableList.of(CompilationUnitEditor.class.getName(), "org.eclipse.wb.internal.core.editor.multi.DesignerEditor"); private final CompletionRcpPreferences preferences; private final IAstProvider astProvider; private final SharedImages images; private final Map<CompletionContextKey, ICompletionContextFunction> functions; private final Provider<IEditorPart> editorProvider; private final IProcessableProposalFactory proposalFactory = new ProcessableProposalFactory(); private final Set<SessionProcessor> processors = Sets.newLinkedHashSet(); private final Set<SessionProcessor> activeProcessors = Sets.newLinkedHashSet(); // Set in storeContext public JavaContentAssistInvocationContext jdtContext; public IRecommendersCompletionContext crContext; public ContentAssistantFacade contentAssist; @Inject public IntelligentCompletionProposalComputer(CompletionRcpPreferences preferences, IAstProvider astProvider, SharedImages images, Map<CompletionContextKey, ICompletionContextFunction> functions, Provider<IEditorPart> editorRetriever) { this.preferences = preferences; this.astProvider = astProvider; this.images = images; this.functions = functions; this.editorProvider = editorRetriever; } @Override public void sessionStarted() { processors.clear(); for (SessionProcessorDescriptor d : preferences.getEnabledSessionProcessors()) { try { processors.add(d.getProcessor()); } catch (Throwable e) { log(ERROR_SESSION_PROCESSOR_FAILED, e, d.getId()); } } activeProcessors.clear(); activeProcessors.addAll(processors); // code looks odd? This method unregisters this instance from the last(!) source viewer see // unregisterCompletionListener for details unregisterCompletionListener(); } @Override public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) { if (!(context instanceof JavaContentAssistInvocationContext)) { return Collections.emptyList(); } storeContext(context); if (!isTriggeredInJavaProject()) { // We can't make recommendations. Fall back to JDT. if (!isContentAssistConfigurationOkay()) { // JDT is still active, so don't add any proposals. return Collections.emptyList(); } else { // JDT is inactive. Return all proposals JDT would have made. List<ICompletionProposal> res = Lists.newLinkedList(); for (Entry<IJavaCompletionProposal, CompletionProposal> pair : crContext.getProposals().entrySet()) { IJavaCompletionProposal jdtProposal = create(pair.getValue(), pair.getKey(), jdtContext, proposalFactory); res.add(jdtProposal); } return res; } } if (!isContentAssistConfigurationOkay()) { enableRecommenders(); int offset = context.getInvocationOffset(); EnabledCompletionProposal info = new EnabledCompletionProposal(images, offset); boolean hasOtherProposals = !crContext.getProposals().isEmpty(); if (hasOtherProposals) { // Return the configure proposal return ImmutableList.<ICompletionProposal>of(info); } else { return ImmutableList.<ICompletionProposal>of(info, new EmptyCompletionProposal(offset)); } } else { List<ICompletionProposal> res = Lists.newLinkedList(); registerCompletionListener(); crContext.set(ACTIVE_PROCESSORS, ImmutableSet.copyOf(activeProcessors)); fireInitializeContext(crContext); fireStartSession(crContext); for (Entry<IJavaCompletionProposal, CompletionProposal> pair : crContext.getProposals().entrySet()) { IJavaCompletionProposal jdtProposal = create(pair.getValue(), pair.getKey(), jdtContext, proposalFactory); res.add(jdtProposal); if (jdtProposal instanceof JavaMethodCompletionProposal) { int position = guessContextInformationPosition(jdtContext); JavaMethodCompletionProposal jmcp = (JavaMethodCompletionProposal) jdtProposal; jmcp.setContextInformationPosition(position); } if (jdtProposal instanceof IProcessableProposal) { IProcessableProposal crProposal = (IProcessableProposal) jdtProposal; crProposal.setTag(CONTEXT, crContext); crProposal.setTag(IS_VISIBLE, true); crProposal.setTag(JDT_UI_PROPOSAL, pair.getKey()); crProposal.setTag(JDT_CORE_PROPOSAL, pair.getValue()); crProposal.setTag(JDT_SCORE, jdtProposal.getRelevance()); fireProcessProposal(crProposal); } } fireEndComputation(res); fireAboutToShow(res); return res; } } private boolean isTriggeredInJavaProject() { if (jdtContext == null) { return false; } IEditorPart editor = editorProvider.get(); if (editor == null) { return false; } if (!JAVA_EDITOR_WHITELIST.contains(editor.getClass().getName())) { return false; } IJavaProject project = jdtContext.getProject(); if (project == null) { return false; } return project.exists(); } private void enableRecommenders() { new DisableContentAssistCategoryJob(MYLYN_ALL_CATEGORY).schedule(); new DisableContentAssistCategoryJob(JDT_ALL_CATEGORY).schedule(); new DisableContentAssistCategoryJob(JDT_TYPE_CATEGORY).schedule(); new DisableContentAssistCategoryJob(JDT_NON_TYPE_CATEGORY).schedule(); } @Override public void sessionEnded() { fireAboutToClose(); } private void storeContext(ContentAssistInvocationContext context) { jdtContext = cast(context); crContext = new RecommendersCompletionContext(jdtContext, astProvider, functions); } protected boolean isContentAssistConfigurationOkay() { Set<String> excludedCategories = Sets.newHashSet(PreferenceConstants.getExcludedCompletionProposalCategories()); if (excludedCategories.contains(RECOMMENDERS_ALL_CATEGORY_ID)) { // If we are excluded on the default tab, then we cannot be on the default tab now, as we are executing. // Hence, we must be on a subsequent tab. return true; } if (isJdtJavaProposalsEnabled(excludedCategories) || isMylynJavaProposalsEnabled(excludedCategories)) { return false; } return true; } private boolean isMylynJavaProposalsEnabled(Set<String> excludedCategories) { return isMylynInstalled() && !excludedCategories.contains(MYLYN_ALL_CATEGORY); } private boolean isJdtJavaProposalsEnabled(Set<String> excludedCategories) { return !excludedCategories.contains(JDT_ALL_CATEGORY) || !excludedCategories.contains(JDT_TYPE_CATEGORY) || !excludedCategories.contains(JDT_NON_TYPE_CATEGORY); } private boolean isMylynInstalled() { CompletionProposalComputerRegistry reg = CompletionProposalComputerRegistry.getDefault(); for (CompletionProposalCategory cat : reg.getProposalCategories()) { if (cat.getId().equals(MYLYN_ALL_CATEGORY)) { return true; } } return false; } private void registerCompletionListener() { ITextViewer v = jdtContext.getViewer(); if (!(v instanceof SourceViewer)) { return; } SourceViewer sv = (SourceViewer) v; contentAssist = sv.getContentAssistantFacade(); contentAssist.addCompletionListener(this); } /* * Unregisters this computer from the last known content assist facade. This method is called in some unexpected * places (i.e., not in sessionEnded and similar methods) because unregistering in these methods would be too early * to get notified about apply events. */ private void unregisterCompletionListener() { if (contentAssist != null) { contentAssist.removeCompletionListener(this); } } protected void fireInitializeContext(IRecommendersCompletionContext crContext) { for (Iterator<SessionProcessor> it = activeProcessors.iterator(); it.hasNext();) { SessionProcessor p = it.next(); try { p.initializeContext(crContext); } catch (Throwable e) { it.remove(); Logs.log(ERROR_SESSION_PROCESSOR_FAILED, e, p.getClass()); } } } protected void fireStartSession(IRecommendersCompletionContext crContext) { for (Iterator<SessionProcessor> it = activeProcessors.iterator(); it.hasNext();) { SessionProcessor p = it.next(); try { boolean interested = p.startSession(crContext); if (!interested) { it.remove(); } } catch (Throwable e) { it.remove(); Logs.log(ERROR_SESSION_PROCESSOR_FAILED, e, p.getClass()); } } } protected void fireProcessProposal(IProcessableProposal proposal) { for (SessionProcessor p : activeProcessors) { try { proposal.getRelevance(); p.process(proposal); } catch (Throwable e) { log(ERROR_SESSION_PROCESSOR_FAILED, e, p.getClass()); } } proposal.getProposalProcessorManager().prefixChanged(crContext.getPrefix()); } protected void fireEndComputation(List<ICompletionProposal> proposals) { for (SessionProcessor p : activeProcessors) { try { p.endSession(proposals); } catch (Throwable e) { log(ERROR_SESSION_PROCESSOR_FAILED, e, p.getClass()); } } } protected void fireAboutToShow(List<ICompletionProposal> proposals) { for (SessionProcessor p : activeProcessors) { try { p.aboutToShow(proposals); } catch (Throwable e) { log(ERROR_SESSION_PROCESSOR_FAILED, e, p.getClass()); } } } protected void fireAboutToClose() { for (SessionProcessor p : activeProcessors) { try { p.aboutToClose(); } catch (Throwable e) { log(ERROR_SESSION_PROCESSOR_FAILED, e, p.getClass()); } } } @Override public void assistSessionStarted(ContentAssistEvent event) { // ignore } @Override public void assistSessionEnded(ContentAssistEvent event) { // ignore // Calling unregister here seems like a good choice here but unfortunately isn't. "proposal applied" events are // fired after the sessionEnded event, and thus, we cannot use this method to unsubscribe from the current // editor. See unregisterCompletionListern for details. } @Override public void selectionChanged(ICompletionProposal proposal, boolean smartToggle) { for (SessionProcessor p : activeProcessors) { try { p.selected(proposal); } catch (Throwable e) { log(ERROR_SESSION_PROCESSOR_FAILED, e, p.getClass()); } } } @Override public void applied(ICompletionProposal proposal) { for (SessionProcessor p : activeProcessors) { try { p.applied(proposal); } catch (Throwable e) { log(ERROR_SESSION_PROCESSOR_FAILED, e, p.getClass()); } } unregisterCompletionListener(); } }