/* * 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.idea.core.psi.impl; import com.intellij.openapi.actionSystem.LangDataKeys; import com.intellij.openapi.application.TransactionGuard; import com.intellij.openapi.components.AbstractProjectComponent; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorDataProvider; import com.intellij.openapi.fileEditor.FileEditorDataProviderManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiTreeChangeEvent; import com.intellij.psi.impl.PsiManagerEx; import com.intellij.psi.impl.PsiManagerImpl; import com.intellij.psi.impl.PsiModificationTrackerImpl; import com.intellij.psi.impl.PsiTreeChangeEventImpl; import com.intellij.psi.impl.file.impl.FileManager; import jetbrains.mps.ide.project.ProjectHelper; import jetbrains.mps.idea.core.psi.MPS2PsiMapperUtil; import jetbrains.mps.idea.core.psi.MPSPsiNodeFactory; import jetbrains.mps.idea.core.psi.impl.events.SModelEventProcessor; import jetbrains.mps.idea.core.psi.impl.events.SModelEventProcessor.ModelProvider; import jetbrains.mps.idea.core.psi.impl.events.SModelEventProcessor.ReloadableModel; import jetbrains.mps.nodefs.MPSNodeVirtualFile; import jetbrains.mps.smodel.GlobalSModelEventsManager; import jetbrains.mps.smodel.ModelAccessHelper; import jetbrains.mps.smodel.event.SModelCommandListener; import jetbrains.mps.smodel.event.SModelEvent; import jetbrains.mps.util.Computable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SAbstractConcept; import org.jetbrains.mps.openapi.language.SConcept; import org.jetbrains.mps.openapi.model.EditableSModel; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeId; import org.jetbrains.mps.openapi.model.SNodeReference; import org.jetbrains.mps.openapi.module.SModule; import org.jetbrains.mps.openapi.module.SModuleListenerBase; import org.jetbrains.mps.openapi.module.SRepository; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; /** * evgeny, 1/25/13 */ public class MPSPsiProvider extends AbstractProjectComponent { // TODO softReference.. private final ConcurrentMap<SModelReference, MPSPsiModel> models = new ConcurrentHashMap<SModelReference, MPSPsiModel>(); private final PsiModificationTrackerImpl myModificationTracker; public static MPSPsiProvider getInstance(@NotNull Project project) { return project.getComponent(MPSPsiProvider.class); } private SModelEventProcessor myEventProcessor; /** * We're notifying about changes in PSI via * {@link com.intellij.psi.impl.PsiModificationTrackerImpl#incCounter()}). * It asserts {@link com.intellij.openapi.application.TransactionGuardImpl#assertWriteActionAllowed() * The problem is the command which created the events we're reacting to might have been either * inside a transaction or not. Currently, most MPS actions don't invoke a transaction. Moreover, * calls to runWriteInEDT() and the like, which happen to exist in MPS actions, run the given code * via LaterInvocator and the code is executed not in a transaction. * <p> * Thus, we should expect both scenarios. * <p> * In the future, maybe we should drop the case when we're creating our own fake transaction here. * It can be done either by removing runWriteInEDT in actions (because every action is wrapped * in {@link com.intellij.openapi.application.TransactionGuardImpl#performUserActivity(Runnable)}) * or by invoking a transaction explicitly in the action. */ private SModelCommandListener myListener = new SModelCommandListener() { public void eventsHappenedInCommand(List<SModelEvent> events) { Runnable processEvents = () -> myEventProcessor.process(events); if (TransactionGuard.getInstance().getContextTransaction() != null) { // the command that caused the events was in a transaction processEvents.run(); } else { // hackish, might be dropped in the future TransactionGuard.submitTransaction(myProject, () -> { SRepository repository = ProjectHelper.getProjectRepository(myProject); if (repository != null) { repository.getModelAccess().runWriteAction(processEvents); } }); } // TODO PsiModificationTrackerImpl.incCounter/incOutOfCodeBlockModificationCounter (see JavaCodeBlockModificationListener) // TODO notify ANY_PSI_CHANGE_TOPIC } }; protected MPSPsiProvider(Project project) { super(project); myEventProcessor = createEventProcessor(); PsiManager psiManager = PsiManagerEx.getInstance(project); this.myModificationTracker = (PsiModificationTrackerImpl) psiManager.getModificationTracker(); } public void initComponent() { GlobalSModelEventsManager.getInstance().addGlobalCommandListener(myListener); FileEditorDataProviderManager.getInstance(myProject).registerDataProvider(new PsiFileEditorDataProvider(), null); } public void disposeComponent() { GlobalSModelEventsManager.getInstance().removeGlobalCommandListener(myListener); } public PsiElement getPsi(SNodeReference nodeRef) { if (nodeRef == null) return null; final SNode node = nodeRef.resolve(ProjectHelper.getProjectRepository(myProject)); if (node == null) return null; return getPsi(node); } public PsiElement getPsi(SNode node) { if (node == null) return null; // give chance to other to tell us what the PSI element is PsiElement source = MPS2PsiMapperUtil.getPsiElement(node, myProject); if (source != null) { return source; } final SModel containingModel = node.getModel(); if (containingModel == null) return null; MPSPsiModel psiModel = getPsi(containingModel); if (psiModel == null) return null; return psiModel.lookupNode(node.getNodeId()); } public MPSPsiModel getPsi(SModel model) { // TODO check GlobalSearchScope.projectScope(myProject).contains(modelFile) final SModelReference modelRef = model.getReference(); MPSPsiModel cached = models.get(modelRef); if (cached != null) return cached; return getMPSPsiModel(model, modelRef); } public MPSPsiModel getPsi(SModelReference modelRef) { MPSPsiModel cached = models.get(modelRef); if (cached != null) return cached; SModel model = modelRef.resolve(ProjectHelper.getProjectRepository(myProject)); // TODO check if the model is valid return getMPSPsiModel(model, modelRef); } public MPSPsiNode create(SNodeId id, SConcept concept, String containingRole) { for (MPSPsiNodeFactory factory : MPSPsiNodeFactory.EP_NAME.getExtensions()) { final MPSPsiNode psiNode = factory.create(id, concept, containingRole, PsiManager.getInstance(myProject)); if (psiNode != null) { return psiNode; } } return new MPSPsiNode(id, concept.getQualifiedName(), containingRole, PsiManager.getInstance(myProject)); } public MPSPsiRef createReferenceNode(String role, SAbstractConcept linkTargetConcept, SModelReference targetModel, SNodeId targetId) { if (linkTargetConcept != null) { for (MPSPsiNodeFactory factory : MPSPsiNodeFactory.EP_NAME.getExtensions()) { final MPSPsiRef psiRefNode = factory.createReferenceNode(role, linkTargetConcept, targetModel, targetId, PsiManager.getInstance(myProject)); if (psiRefNode != null) { return psiRefNode; } } } return new MPSPsiRef(role, targetModel, targetId, PsiManager.getInstance(myProject)); } public MPSPsiRef createReferenceNode(String role, SAbstractConcept linkTargetConcept, String referenceText) { if (linkTargetConcept != null) { for (MPSPsiNodeFactory factory : MPSPsiNodeFactory.EP_NAME.getExtensions()) { final MPSPsiRef psiRefNode = factory.createReferenceNode(role, linkTargetConcept, referenceText, PsiManager.getInstance(myProject)); if (psiRefNode != null) { return psiRefNode; } } } return new MPSPsiRef(role, referenceText, PsiManager.getInstance(myProject)); } private MPSPsiModel getMPSPsiModel(final SModel model, final SModelReference modelRef) { if (MPS2PsiMapperUtil.hasCorrespondingPsi(model)) return null; // synchronizing by model: // we guard MPSPsiModel.reload() exactly by model, // on the other hand, the key in models is modelRef, but different models in one repo seem to always have // different modelRefs synchronized (model) { MPSPsiModel result = models.get(modelRef); if (result == null) { result = new MPSPsiModel(modelRef, PsiManager.getInstance(myProject)); // since some time ago we have to guard MPSPsiModel in this way: // MPSPsiModel.reload() should not happen for different instances, which are connected // to one SModel, because root nodes are re-used in case of file-per-root persistence. // I.e. those root nodes cannot be added as children to a model when they are already children of another result.reload(model); models.put(modelRef, result); model.getModule().addModuleListener(new SModuleListenerBase() { @Override public void beforeModelRenamed(SModule module, SModel model, SModelReference newRef) { models.remove(model.getReference()); } @Override public void beforeModelRemoved(SModule module, SModel removedModel) { if (removedModel != model) return; models.remove(modelRef); } }); model.addModelListener(myProject.getComponent(PsiModelReloadListener.class)); } return result; } } private SModelEventProcessor createEventProcessor() { return new SModelEventProcessor(new ModelProvider() { @Override public ReloadableModel lookupModel(SModelReference modelReference) { // must be alright concurrency-wise, because ConcurrentHashMap creates a memory barrier final MPSPsiModel psiModel = models.get(modelReference); if (psiModel == null) return null; // MPPsiModel.reload() relies on roots' virtual files being up-to-date, so we save the model in case // root name might have changed return new ReloadableModel() { @Override public void reload(SNodeId sNodeId) { MPSPsiNode oldPsiNode = psiModel.lookupNode(sNodeId); if (oldPsiNode != null && psiModel.isRoot(oldPsiNode)) { // sNodeId corresponds to root node save(psiModel); } MPSPsiNode psiNode = psiModel.reload(sNodeId); notifyPsiChanged(psiModel, psiNode); } @Override public void reloadAll() { save(psiModel); psiModel.reloadAll(); notifyPsiChanged(psiModel, null); } private void save(MPSPsiModel psiModel) { SModel smodel = psiModel.getSModelReference().resolve(ProjectHelper.getProjectRepository(psiModel.getProject())); if (smodel instanceof EditableSModel) { ((EditableSModel) smodel).save(); } } }; } }); } void notifyPsiChanged(MPSPsiModel model, MPSPsiNodeBase node) { if (!model.isValid()) return; PsiManager manager = model.getManager(); if (manager == null || !(manager instanceof PsiManagerImpl)) return; myModificationTracker.incCounter(); // TODO: this is a dumb straightforward solution, better use beforeChage*. Or not? manager.dropResolveCaches(); PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager); event.setParent(node != null ? node : model); event.setGenericChange(false); ((PsiManagerImpl) manager).childrenChanged(event); } void notifyModelRenamed(MPSPsiModel model, String oldName, String newName) { PsiManager manager = model.getManager(); if (manager == null || !(manager instanceof PsiManagerImpl)) return; myModificationTracker.incCounter(); // TODO: this is a dumb straightforward solution, better use beforeChage*. Or not? manager.dropResolveCaches(); PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager); event.setElement(model); event.setPropertyName(PsiTreeChangeEvent.PROP_FILE_NAME); event.setOldValue(oldName); event.setNewValue(newName); ((PsiManagerImpl) manager).propertyChanged(event); } private class PsiFileEditorDataProvider implements FileEditorDataProvider { @Nullable @Override public Object getData(String dataId, FileEditor e, VirtualFile file) { if (!file.isValid()) return null; // if (LangDataKeys.PSI_FILE.is(dataId)) { // return getPsiFile(file); // } if (LangDataKeys.PSI_ELEMENT.is(dataId)) { return getPsiPsiElement(file); } return null; } private PsiElement getPsiPsiElement(VirtualFile snodeVFile) { if (snodeVFile instanceof MPSNodeVirtualFile) { final MPSNodeVirtualFile mpsFile = (MPSNodeVirtualFile) snodeVFile; final SNodeReference sNodePointer = mpsFile.getSNodePointer(); MPSPsiModel psiModel = models.get(sNodePointer.getModelReference()); if (psiModel == null) return null; PsiElement psiElement = new ModelAccessHelper(ProjectHelper.getModelAccess(myProject)).runReadAction(new Computable<PsiElement>() { @Override public PsiElement compute() { return getPsi(sNodePointer); } }); if (psiElement != null) return psiElement; for (MPSPsiRootNode rootNode : psiModel.getChildren(MPSPsiRootNode.class)) { if (rootNode.getSNodeReference().equals(mpsFile.getSNodePointer())) { return rootNode; } } // TODO not cached node } return null; } private PsiFile getPsiFile(VirtualFile snodeVFile) { if (snodeVFile instanceof MPSNodeVirtualFile) { final MPSNodeVirtualFile mpsFile = (MPSNodeVirtualFile) snodeVFile; SNodeReference sNodePointer = mpsFile.getSNodePointer(); MPSPsiModel mpsPsiModel = models.get(sNodePointer.getModelReference()); if (mpsPsiModel == null) return null; VirtualFile sourceVFile = mpsPsiModel.getSourceVirtualFile(); FileManager fileManager = ((PsiManagerEx) PsiManagerEx.getInstance(myProject)).getFileManager(); return fileManager.findFile(sourceVFile); // TODO not cached node } return null; } } }