/* * Copyright 2003-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 * * 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 jetbrains.mps.intentions; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ApplicationComponent; import com.intellij.openapi.components.PersistentStateComponent; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import jetbrains.mps.errors.QuickFixProvider; import jetbrains.mps.ide.MPSCoreComponents; import jetbrains.mps.intentions.IntentionsVisitor.CollectAvailableIntentionsVisitor; import jetbrains.mps.intentions.IntentionsVisitor.GetHighestAvailableIntentionTypeVisitor; import jetbrains.mps.lang.script.runtime.AbstractMigrationRefactoring; import jetbrains.mps.lang.script.runtime.RefactoringScript; import jetbrains.mps.lang.script.runtime.ScriptAspectDescriptor; import jetbrains.mps.nodeEditor.EditorComponent; import jetbrains.mps.nodeEditor.EditorMessage; import jetbrains.mps.openapi.editor.Editor; import jetbrains.mps.openapi.editor.EditorContext; import jetbrains.mps.openapi.editor.message.SimpleEditorMessage; import jetbrains.mps.openapi.intentions.IntentionAspectDescriptor; import jetbrains.mps.openapi.intentions.IntentionDescriptor; import jetbrains.mps.openapi.intentions.IntentionExecutable; import jetbrains.mps.openapi.intentions.IntentionFactory; import jetbrains.mps.openapi.intentions.Kind; import jetbrains.mps.smodel.MPSModuleRepository; import jetbrains.mps.smodel.ModelAccessHelper; import jetbrains.mps.smodel.SLanguageHierarchy; import jetbrains.mps.smodel.SModelOperations; import jetbrains.mps.smodel.language.LanguageRegistry; import jetbrains.mps.smodel.language.LanguageRuntime; import jetbrains.mps.typesystem.inference.ITypeContextOwner; import jetbrains.mps.typesystem.inference.TypeContextManager; import jetbrains.mps.util.Computable; import jetbrains.mps.util.Pair; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SAbstractConcept; import org.jetbrains.mps.openapi.language.SLanguage; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.module.SModuleReference; import org.jetbrains.mps.util.DepthFirstConceptIterator; import org.jetbrains.mps.util.UniqueIterator; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @State( name = "MPSIntentionsManager", storages = @Storage("intentions.xml") ) public class IntentionsManager implements ApplicationComponent, PersistentStateComponent<IntentionsManager.MyState> { private static final Logger LOG = LogManager.getLogger(IntentionsManager.class); public static String getDescriptorClassName(SModuleReference langRef) { return "IntentionsDescriptor"; } public static IntentionsManager getInstance() { return ApplicationManager.getApplication().getComponent(IntentionsManager.class); } private MyState myState = new MyState(); /** * FIXME this field is here just for the sake of ModelAccess, ApplicationComponent shall not depend on any project-related stuff, * rather shall get it from context. */ private final MPSModuleRepository myRepository; public IntentionsManager(MPSCoreComponents coreComponents) { myRepository = coreComponents.getModuleRepository(); } public synchronized Kind getHighestAvailableBaseIntentionType(final SNode node, final EditorContext editorContext) { final GetHighestAvailableIntentionTypeVisitor visitor = new GetHighestAvailableIntentionTypeVisitor(); TypeContextManager.getInstance().runTypecheckingAction((ITypeContextOwner) editorContext.getEditorComponent(), new Runnable() { @Override public void run() { Filter filter = new Filter(getDisabledIntentions()) { @Override boolean accept(IntentionFactory intentionFactory) { return super.accept(intentionFactory) && visitor.hasHigherPriority(intentionFactory.getKind()); } }; for (SNode currentNode = node; currentNode != null; currentNode = currentNode.getParent()) { if (!visitIntentions(currentNode, visitor, filter, currentNode != node, editorContext)) { break; } } } }); return visitor.getIntentionKind(); } public synchronized Collection<Pair<IntentionExecutable, SNode>> getAvailableIntentions(final QueryDescriptor query, @NotNull final SNode node, final EditorContext context) { return TypeContextManager.getInstance().runTypecheckingAction((ITypeContextOwner) context.getEditorComponent(), new Computable<Collection<Pair<IntentionExecutable, SNode>>>() { @Override public Set<Pair<IntentionExecutable, SNode>> compute() { // Hiding intentions with same IntentionDescriptor // important when currently selected element and it's parent has same intention final Set<IntentionDescriptor> processedIntentionDescriptors = new HashSet<>(); Filter filter = new Filter(query.myEnabledOnly ? getDisabledIntentions() : null, query.mySurroundWith) { @Override boolean accept(IntentionFactory intentionFactory) { return super.accept(intentionFactory) && !processedIntentionDescriptors.contains(intentionFactory); } }; Set<Pair<IntentionExecutable, SNode>> result = new HashSet<>(); for (IntentionExecutable intentionExecutable : getAvailableIntentionsForExactNode(node, context, false, filter)) { result.add(new Pair<>(intentionExecutable, node)); processedIntentionDescriptors.add(intentionExecutable.getDescriptor()); } if (!query.isCurrentNodeOnly()) { SNode parent = node.getParent(); while (parent != null) { for (IntentionExecutable intentionExecutable : getAvailableIntentionsForExactNode( parent, context, true, filter)) { result.add(new Pair<>(intentionExecutable, parent)); processedIntentionDescriptors.add(intentionExecutable.getDescriptor()); } parent = parent.getParent(); } } return result; } }); } private List<IntentionExecutable> getAvailableIntentionsForExactNode(@NotNull final SNode node, @NotNull final EditorContext context, boolean isAncestor, Filter filter) { CollectAvailableIntentionsVisitor visitor = new CollectAvailableIntentionsVisitor(); visitIntentions(node, visitor, filter, isAncestor, context); List<IntentionExecutable> result = new ArrayList<>(); for (IntentionFactory factory : visitor.getAvailableIntentionFactories()) { try { result.addAll(factory.instances(node, context)); } catch (Throwable t) { LOG.error("Exception during parameterized intentions instantiation", t); } } List<SimpleEditorMessage> messages = ((EditorComponent) context.getEditorComponent()).getHighlightManager().getMessagesFor(node); for (SimpleEditorMessage message : messages) { //TODO remove this cast List<QuickFixProvider> intentionProviders = ((EditorMessage) message).getIntentionProviders(); for (QuickFixProvider intentionProvider : intentionProviders) { QuickFixAdapter intention = new QuickFixAdapter(intentionProvider.getQuickFix(), intentionProvider.isError()); if ((isAncestor && !intention.isAvailableInChildNodes()) || !intention.isApplicable(node, context)) { continue; } try { result.addAll(intention.instances(node, context)); } catch (Throwable t) { LOG.error("Exception during parameterized intentions instantiation", t); } } } return result; } public synchronized boolean isIntentionDisabled(String persistentStateKey) { return getDisabledIntentions().contains(persistentStateKey); } private Set<String> getDisabledIntentions() { return myState.getDisabledIntentions(); } //-------------disabled state control----------------- public void setIntentionState(String persistentStateKey, boolean disabled) { if (disabled) { disableIntention(persistentStateKey); } else { enableIntention(persistentStateKey); } } public synchronized void disableIntention(String persistentStateKey) { getDisabledIntentions().add(persistentStateKey); } public synchronized void enableIntention(String persistentStateKey) { getDisabledIntentions().remove(persistentStateKey); } //-------------node info by intention----------------- /** * Returning combined sorted list of all {@link IntentionFactory} available in the current repository. * This list will be first sorted by the language FQ name and then sorted by presentation of each intention, * so result can be easily displayed in UI components. * * @return combined sorted list of all available {@link IntentionFactory} */ @NotNull public synchronized Map<LanguageRuntime, Collection<IntentionFactory>> getAllIntentionFactories() { return new ModelAccessHelper(myRepository).runReadAction(() -> { Map<LanguageRuntime, Collection<IntentionFactory>> result = new HashMap<>(); for (LanguageRuntime lr : LanguageRegistry.getInstance(myRepository).getAvailableLanguages()) { List<IntentionFactory> languageIntentions = new ArrayList<>(); result.put(lr, languageIntentions); IntentionAspectDescriptor intentionAspect = lr.getAspect(IntentionAspectDescriptor.class); if (intentionAspect != null) { languageIntentions.addAll(intentionAspect.getAllIntentions()); } else { jetbrains.mps.intentions.IntentionAspectDescriptor compatibilityIntentionAspect = lr.getAspect(jetbrains.mps.intentions.IntentionAspectDescriptor.class); if (compatibilityIntentionAspect != null) { languageIntentions.addAll(compatibilityIntentionAspect.getAllAPIIntentions()); } } final ScriptAspectDescriptor scriptAspect = lr.getAspect(ScriptAspectDescriptor.class); if (scriptAspect != null) { languageIntentions.addAll(new MigrationRefactoringIntentions(lr, scriptAspect).getIntentions()); } } return result; }); } //-------------visiting registered intentions--------------- private boolean visitIntentions(SNode node, IntentionsVisitor visitor, Filter filter, boolean isAncestor, EditorContext editorContext) { if (node.getModel() == null) { return true; } LanguageRegistry languageRegistry = LanguageRegistry.getInstance(editorContext.getRepository()); // respect intentions from imported languages only ArrayList<IntentionAspectDescriptor> activeIntentionAspects = new ArrayList<>(); ArrayList<jetbrains.mps.intentions.IntentionAspectDescriptor> activeCompatibilityIntentionAspects = new ArrayList<>(); // respect migration scripts from imported languages only ArrayList<MigrationRefactoringIntentions> activeIntentionsFromMigrationScripts = new ArrayList<>(); for (SLanguage l : new SLanguageHierarchy(languageRegistry, SModelOperations.getAllLanguageImports(node.getModel())).getExtended()) { final LanguageRuntime lr = languageRegistry.getLanguage(l); if (lr == null) { continue; } final IntentionAspectDescriptor intentionAspect = lr.getAspect(IntentionAspectDescriptor.class); if (intentionAspect != null) { activeIntentionAspects.add(intentionAspect); } else { jetbrains.mps.intentions.IntentionAspectDescriptor compatibiltiyIA = lr.getAspect(jetbrains.mps.intentions.IntentionAspectDescriptor.class); if (compatibiltiyIA != null) { activeCompatibilityIntentionAspects.add(compatibiltiyIA); } } final ScriptAspectDescriptor scriptsAspect = lr.getAspect(ScriptAspectDescriptor.class); if (scriptsAspect != null) { activeIntentionsFromMigrationScripts.add(new MigrationRefactoringIntentions(lr, scriptsAspect)); } } // there's no special meaning in using depth-first iterator, it's just the only one available at the moment // and looks pretty reasonable for the task (super-concepts first, then implemented interfaces) for (SAbstractConcept concept : new UniqueIterator<>(new DepthFirstConceptIterator(node.getConcept()))) { ArrayList<IntentionFactory> intentionsForConcept = new ArrayList<>(); for (IntentionAspectDescriptor intentionAspect : activeIntentionAspects) { final Collection<IntentionFactory> intentions = intentionAspect.getIntentions(concept); if (intentions == null) { continue; } intentionsForConcept.addAll(intentions); } for (jetbrains.mps.intentions.IntentionAspectDescriptor intentionAspect : activeCompatibilityIntentionAspects) { final Collection<IntentionFactory> intentions = intentionAspect.getAPIIntentions(concept); if (intentions == null) { continue; } intentionsForConcept.addAll(intentions); } for (MigrationRefactoringIntentions refactoringIntentions : activeIntentionsFromMigrationScripts) { intentionsForConcept.addAll(refactoringIntentions.getIntentions(concept)); } for (IntentionFactory intentionFactory : intentionsForConcept) { if (isAncestor && !intentionFactory.isAvailableInChildNodes()) { continue; } if (!filter.accept(intentionFactory) || !intentionFactory.isApplicable(node, editorContext)) { continue; } if (!visitor.visit(intentionFactory)) { return false; } } } return true; } static class Filter { private Set<String> myDisabledIntentions = null; private boolean mySurroundWith = false; public Filter(Set<String> disabledIntentions) { myDisabledIntentions = disabledIntentions; } public Filter(Set<String> disabledIntentions, boolean surroundWith) { this(disabledIntentions); mySurroundWith = surroundWith; } boolean accept(IntentionFactory intentionFactory) { if (myDisabledIntentions != null && myDisabledIntentions.contains(intentionFactory.getPersistentStateKey())) { return false; } return intentionFactory.isSurroundWith() ? mySurroundWith : !mySurroundWith; } } //-------------queryDescriptor----------------- public static class QueryDescriptor { private boolean myEnabledOnly = false; private boolean myCurrentNodeOnly = false; private boolean mySurroundWith = false; public QueryDescriptor() { } public void setSurroundWith(boolean surroundWith) { mySurroundWith = surroundWith; } public void setEnabledOnly(boolean enabledOnly) { myEnabledOnly = enabledOnly; } public void setCurrentNodeOnly(boolean currentNodeOnly) { myCurrentNodeOnly = currentNodeOnly; } public boolean isCurrentNodeOnly() { return myCurrentNodeOnly; } } //-------------component methods----------------- @Override public void initComponent() { } @Override @NonNls @NotNull public String getComponentName() { return "MPS Intention Manager"; } @Override public void disposeComponent() { } @Override public MyState getState() { return myState; } @Override public void loadState(MyState state) { myState = state; } public static class MyState { private Set<String> myDisabledIntentions = new HashSet<String>(); public Set<String> getDisabledIntentions() { return myDisabledIntentions; } public void setDisabledIntentions(Set<String> disabledIntentions) { myDisabledIntentions = disabledIntentions; } } /** * Factory and filter by concept for intentions that originate from migration scripts */ private static class MigrationRefactoringIntentions { private final Collection<MigrationRefactoringAdapter> myIntentionAdapters; public MigrationRefactoringIntentions(@NotNull LanguageRuntime lr, @NotNull ScriptAspectDescriptor scriptAspect) { ArrayList<MigrationRefactoringAdapter> adapters = new ArrayList<>(); for (RefactoringScript rs : scriptAspect.getRefactoringScripts()) { for (AbstractMigrationRefactoring refactoring : rs.getRefactorings()) { if (refactoring.isShowAsIntention()) { adapters.add(new MigrationRefactoringAdapter(refactoring, rs.getScriptNode())); } } } myIntentionAdapters = adapters.isEmpty() ? Collections.emptyList() : adapters; } @NotNull public Collection<? extends IntentionFactory> getIntentions() { return myIntentionAdapters; } @NotNull public Collection<IntentionFactory> getIntentions(@NotNull SAbstractConcept concept) { if (myIntentionAdapters.isEmpty()) { return Collections.emptyList(); } ArrayList<IntentionFactory> rv = new ArrayList<>(myIntentionAdapters.size()); for (MigrationRefactoringAdapter a : myIntentionAdapters) { // don't want to use IntentionDescriptor.getConcept():String, thus exposed AbstractMigrationRefactoring if (a.getRefactoring().getApplicableConcept().equals(concept)) { rv.add(a); } } return rv; } } @Nullable public IntentionExecutable getIntentionById(SNode node, Editor editor, String id) { return getIntentionById(node, editor.getEditorContext(), id); } /** * @return the matching intention, if found <em>and applicable</em>, {@code null} otherwise */ @Nullable public IntentionExecutable getIntentionById(SNode node, EditorContext editorContext, String id) { List<IntentionExecutable> result = getIntentionsById(node, editorContext, id); assert result.size() <= 1; return result.isEmpty() ? null : result.get(0); } @NotNull public List<IntentionExecutable> getIntentionsById(SNode node, EditorContext editorContext, String id) { QueryDescriptor query = new QueryDescriptor(); query.setCurrentNodeOnly(true); Collection<Pair<IntentionExecutable, SNode>> intentions = getAvailableIntentions(query, node, editorContext); List<IntentionExecutable> result = new ArrayList<>(); for (Pair<IntentionExecutable, SNode> intention : intentions) { if (intention.o1.getDescriptor().getPersistentStateKey().equals(id)) { result.add(intention.o1); } } return result; } }